# Notebook from sparsh-ai/recohut
Path: _docs/nbs/reco-tut-ttr-t3-01-preprocessing.ipynb

import os
project_name = "reco-tut-ttr"; branch = "main"; account = "sparsh-ai"
project_path = os.path.join('/content', project_name)

if not os.path.exists(project_path):
    !cp /content/drive/MyDrive/mykeys.py /content
    import mykeys
    !rm /content/mykeys.py
    path = "/content/" + project_name;
    !mkdir "{path}"
    %cd "{path}"
    import sys; sys.path.append(path)
    !git config --global user.email "recotut@recohut.com"
    !git config --global user.name  "reco-tut"
    !git init
    !git remote add origin https://"{mykeys.git_token}":x-oauth-basic@github.com/"{account}"/"{project_name}".git
    !git pull origin "{branch}"
    !git checkout main
else:
    %cd "{project_path}"/content/reco-tut-ttr
!git statusOn branch main
Your branch is up to date with 'origin/main'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	data/silver/

nothing added to commit but untracked files present (use "git add" to track)
!git add . && git commit -m 'commit' && git push origin main[main ca3bb40] commit
 2 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 data/silver/item_clicks.parquet.gzip
 create mode 100644 data/silver/item_metadata_dense.parquet.gzip
Counting objects: 6, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 31.98 MiB | 16.83 MiB/s, done.
Total 6 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/sparsh-ai/reco-tut-ttr.git
   7ac3f2f..ca3bb40  main -> main

---_____no_output_____## Setup_____no_output_____

!pip install -q git+https://github.com/sparsh-ai/recochef.git  Installing build dependencies ... done
  Getting requirements to build wheel ... done
    Preparing wheel metadata ... done
     |████████████████████████████████| 4.3 MB 5.1 MB/s
  Building wheel for recochef (PEP 517) ... done
#basic
import csv
from os import path, makedirs
import random
import requests
import math
import pickle
import gc
from time import time

# matrix
import pandas as pd
import numpy as np
import networkx as nx
from networkx.algorithms import bipartite
from networkx.algorithms import components

import matplotlib.pyplot as plt

# fancy python styling and error bars
from typing import Dict, List, Set, Optional, Any, Tuple
from collections import defaultdict, Counter
from tqdm import tqdm

from dataclasses import dataclass
from recochef.utils._utils import download_yandex

from gensim.models import Word2Vec
import multiprocessing_____no_output_____RANDOM_SEED = 42_____no_output_____

## Data loading_____no_output_____

class TrivagoData:
    """download the trivago dataset"""

    def __init__(self, tempdata_path='/content/tempdata'):
        self.data_path = tempdata_path
        if not path.exists(self.data_path):
            makedirs(self.data_path)

    def train(self, name='train'):
        url = 'https://disk.yandex.ru/d/fFEvSaj6Cdplug'
        fpath = path.join(self.data_path, name+'.parquet.gzip')
        if not path.exists(fpath):
            download_yandex(url, fpath)
        return pd.read_parquet(fpath)

    def test(self, name='test'):
        url = 'https://disk.yandex.ru/d/_kAw9ajZuIC3rg'
        fpath = path.join(self.data_path, name+'.parquet.gzip')
        if not path.exists(fpath):
            download_yandex(url, fpath)
        return pd.read_parquet(fpath)

    def items(self, name='items'):
        url = 'https://github.com/recohut/reco-data/raw/master/trivago/v1/item_metadata.parquet.gz'
        fpath = path.join(self.data_path, name+'.parquet.gzip')
        if not path.exists(fpath):
            r = requests.get(url, allow_redirects=True)
            open(fpath, 'wb').write(r.content)
        return pd.read_parquet(fpath)

    def confirmation(self, name='confirmation'):
        url = 'https://github.com/recohut/reco-data/raw/master/trivago/v1/confirmation.parquet.gz'
        fpath = path.join(self.data_path, name+'.parquet.gzip')
        if not path.exists(fpath):
            r = requests.get(url, allow_redirects=True)
            open(fpath, 'wb').write(r.content)
        return pd.read_parquet(fpath)

    def validation(self, name='validation'):
        url = 'https://github.com/recohut/reco-data/raw/master/trivago/v1/validation.parquet.gz'
        fpath = path.join(self.data_path, name+'.parquet.gzip')
        if not path.exists(fpath):
            r = requests.get(url, allow_redirects=True)
            open(fpath, 'wb').write(r.content)
        return pd.read_parquet(fpath)_____no_output_____data_loader = TrivagoData()_____no_output_____item_metadata = data_loader.items()
item_metadata.head()_____no_output_____train = data_loader.train()
train.head()_____no_output_____test = data_loader.test()
test.head()_____no_output_____confirmation = data_loader.confirmation()
confirmation.head()_____no_output_____validation = data_loader.validation()
validation.head()_____no_output_____

## Compute CTR_____no_output_____Compute click-through ratios for hotels. Do this two ways.

First, by looping over the clickouts in the training set and computing for each hotel i: *ctr_i = (times hotel i is clicked)/(# times hotel i appears in impression results)*.

Second -- intuitively, we'd like the click through ratio to be penalized most when the first-rank item is not clicked and rewarded most when it is clicked at a higher rank._____no_output_____

compute_ctr: Dict = defaultdict(lambda: {"num":0, "num_pw":0, "denom":0, "denom_pw": 0})
rank_click_prob: Dict[int,float] = {}_____no_output_____# helpers for main function -- these get applied to df rows
def create_entry(interaction_row: Tuple[str,int]) -> None:
    # unpack tuple. (item_id, rank_appeared)
    item_id = interaction_row[0]
    rank_appeared = interaction_row[1]
    entry = compute_ctr[item_id]
    entry["num"] += 1
    entry["num_pw"] += rank_click_prob[rank_appeared]_____no_output_____def count_denom(L: List) -> None:
    for idx, id in enumerate(L):
        compute_ctr[id]["denom"] +=1
        compute_ctr[id]["denom_pw"] += rank_click_prob[idx + 1] # rank = index + 1_____no_output_____df = train.copy()
df.head()_____no_output_____df = df[df.action_type == "clickout item"]
df["impressions"] = df["impressions"].str.split("|").map(list)

# meanwhile: get the index of the reference in the impression. Adding one makes this the rank.
get_rank = lambda x: 1 + x[1].index(x[0]) if x[0] in x[1] else 0
df["rank_appeared"] = df[["reference","impressions"]].apply(get_rank,axis=1)
# apply this over ref and impressions (x[0] is reference, x[1] is impressions)
df = df[df.rank_appeared != 0]  # if the reference is not in the impressions list -- remove it_____no_output_____plt.plot(df["rank_appeared"].value_counts().sort_index())
plt.show()_____no_output_____gc.collect()_____no_output_____# i want the clicks df to look like:
# id | ctr | ctr_prob
# AB | 0.1 | 0.03

denominator = df.shape[0] # how many observations am I working with
rank: int
for rank, count in df["rank_appeared"].value_counts().iteritems():
    rank_click_prob[rank] = 1 - (count/denominator)

#  add these interaction counts to my default dict with this apply trick
df[["reference","rank_appeared"]].apply(create_entry,axis=1)
df["impressions"].apply(count_denom)

# then do the division num/denom and output to a new dataframe
safe_divide = lambda x,y: x / y if (y > 0) else 0
record_format  = [{"item_id": id, "ctr":safe_divide(dict["num"],dict["denom"]), "ctr_prob": safe_divide(dict["num_pw"],dict["denom_pw"])} for id,dict in compute_ctr.items()]
clicks = pd.DataFrame.from_records(record_format)

clicks.head()_____no_output_____silver_layer = './data/silver'
if not path.exists(silver_layer):
    os.makedirs(silver_layer)_____no_output_____# save it
clicks.to_parquet('./data/silver/item_clicks.parquet.gzip', compression='gzip')_____no_output_____

## Extract Hotel features_____no_output_____

RATING_MAP = {"Satisfactory Rating": 1, "Good Rating": 2, "Very Good Rating": 3, "Excellent Rating": 4}
STAR_MAP = {"1 Star": 1, "2 Star": 2, "3 Star": 3, "4 Star": 4, "5 Star": 5}
HOTEL_CAT = {
    "Hotel": "hotel",
    "Resort": "resort",
    "Hostal (ES)": "hostal",
    "Motel": "motel",
    "House / Apartment": "house",
}_____no_output_____def densify(d: Dict[str,Any], properties):
    """
    Apply the mapping d to each row of properties
    """
    values = [None] * properties.shape[0]
    for i, p in enumerate(properties):
        for k in d:
            if k in p:
                values[i] = d[k]
    return values_____no_output_____df = item_metadata.copy()
df.head()_____no_output_____df["properties_temp"] = df["properties"].str.split("|").map(set)
df["rating"] = densify(RATING_MAP, df["properties_temp"])
df["stars"] = densify(STAR_MAP, df["properties_temp"])
df["cat"] = densify(HOTEL_CAT, df["properties_temp"])
df['item_id'] = df['item_id'].astype('str')
df = df.drop(columns=["properties_temp"])
df.head()_____no_output_____df2 = pd.read_parquet('./data/silver/item_clicks.parquet.gzip')
df2.head()_____no_output_____merged = pd.merge(df,df2,on="item_id",how="left")
merged = merged.fillna(value=0)
merged.head()_____no_output_____merged['cat'] = merged['cat'].astype('str')_____no_output_____merged.to_parquet('./data/silver/item_metadata_dense.parquet.gzip', compression='gzip')_____no_output_____

**We have a Trivago dataset with user sessions. What task we are trying to solve?**

We will leverage user profile and session data to create a model which estimates the probability that a person will click-out on a hotel, and use this probability to sort search results.

**What do my labels look like?**
    - Positive labels (1) are click-out events and negative labels are ones which a user did not click (0). So, because there are 25 resuls and one click-out event, there will be 1 positive example for 24 negatives.

**What information do we have while crafting features?**
    - all of the information which precedes the clickout action in the session.
    - entire user profile which a user has previously looked at.

Extract labels and features as follows:
    - for each session id: create an ordered list of sessions.
    - for each user id: construct a profile of all sessions done by that user
    - roll through each session_____no_output_____

# helpers --
def safe_mean(input: List[float]) -> float:
    if len(input) == 0:
        return 0.0
    return sum(input) / len(input)

def sigmoid(x: float) -> float:
  return 1 / (1 + math.exp(-x))

def jaccard(lhs: Set[str], rhs: Set[str]) -> float:
    isect_size = sum(1 for x in lhs if x in rhs)
    union_size = len(lhs.union(rhs))
    return isect_size / union_size

def hotel_sim(id_x: str, id_y: str) -> float:
    """
    Look up the ids of the hotels from the dict and return the jaccard similarity of their properties
    """
    try:
        x_props = id_to_hotel[id_x].properties
        y_props = id_to_hotel[id_y].properties
        return jaccard(x_props,y_props)
    except KeyError:
        # fix these weird dictionary misses
        # print(id_x,id_y)
        return 0

def hotel_sim_embed(id_x: str, id_y: str) -> float:
    """
    Look up the ids of the hotels from the dict and return their embeddding similarity
    """
    try:
        sim: float = w2v_model.wv.similarity(id_x, id_y)
        return sim
    except KeyError:
        return 0.0

# First define classes to make handling data a little easier.
@dataclass
class Hotel:
    item_id: str
    properties: Set[str]
    cat: str
    rating: float
    stars: float
    ctr: float
    ctr_prob: float

@dataclass
class Interaction:
    timestamp: int
    action_type: str
    action_on: str
    is_clickout: bool
    # if it's a clickout event we have more data.
    # what's the best way to say this with dataclasses?
    impressions: List[str]
    prices: List[float]

    def get_clicked_idx(self) -> int:
        """
        Helper to get the index of the clicked item, which is helpful for feature extraction.
        Be careful with this method for 2 reasons:
        1. if it's called on an interaction instance which is not a clickout, raise an error.
        2. using this method improperly in the feature extractor can generate the label for the current prediction task.
            for example: adding this line gives us an auc of 1.
            cheat = (choice_idx == current_clickout.get_clicked_idx())
        """
        assert(self.is_clickout)
        idx: int
        try:
            idx = self.impressions.index(self.action_on)
        except ValueError:
            idx = -1
        return idx

@dataclass(unsafe_hash=True) #TypeError: unhashable type: 'Session' just don't use me as a key
class Session:
    start: int
    user_id: str
    session_id: str
    interactions: List[Interaction]

    def set_user_id(self,num: int) -> "Session":
        """
        This helps us with the *extra training data* strategy where we want copies of
        our session data which don't align with user profiles -- to prevent over-learning
        user-based features while training.
        """
        new_user_id: str = self.user_id + "abc" + str(num)
        new_session_id: str = self.session_id + "abc" + str(num)
        new_session: Session = Session(self.start,new_user_id,new_session_id,self.interactions)
        return new_session

    def append_interaction(self, o: Interaction) -> None:
        """ helper """
        self.interactions.append(o)

# makes less sense to make a dataclass here because user profiles change often
class UserProfile:

    def __init__(self, id: str):
        self.user_id = id
        self.sessions: Dict[str,Session] = {}
        self.unique_interactions: Set[str] = set([])

    def update(self, session_id: str, interaction: Interaction) ->  None:
        """
        Update a user profile requires two things.
        - First: add the reference (action_on) string to the unique_interactions set.
        - Second: find the session which the interaction belongs to and append it. If it doesn't
            exist make a new session with the interaction as the first entry
        """
        if interaction.action_on != "":
            self.unique_interactions.add(interaction.action_on)

        s: Session
        try:
            s = self.sessions[session_id] # try to find the session
            s.append_interaction(interaction)
        except KeyError:
            # but if it doesn't work, create a new user at that address.
            self.sessions[session_id] = Session(interaction.timestamp, self.user_id,session_id,[interaction])

# this one is a wrapper for training and test data types
@dataclass
class SessionData:
    data: pd.DataFrame
    qids: List[str]
    feature_names: List[str]

    def get_session(self, session_id: str) -> Session:
        """
        Helps me commpute mean reciprocal rank later on.
        """
        # access the session dict, then skip to the corresponding step in that session's interaction list
        s_temp: Session = sids_to_data[session_id]
        return s_temp

def create_session(df: List[Dict[str,str]]) -> Session:
    """
    This function takes a list of interactions and saves it as a session in object format.
    """
    interaction_list: List[Interaction] = []

    for d in df:
        i: Interaction
        is_clickout = ("clickout item" == d["action_type"])
        t = int(d["timestamp"])#datetime.fromtimestamp(int(d["timestamp"]))
        if is_clickout:
            i = Interaction(t,d["action_type"], str(d["reference"]),
            is_clickout, d["impressions"].split("|"), [float(i) for i in d["prices"].split("|")])
        else:
            i = Interaction(t,d["action_type"],str(d["reference"]),is_clickout,[],[])

        interaction_list.append(i)
    return Session(interaction_list[0].timestamp,df[0]["user_id"],df[0]["session_id"],interaction_list)

def extract_features(session: Session, step: int, choice_idx: int) -> Dict[str,Any]:
    """
    Feature extraction for one session step of action type 'clicked out'
    """
    current_clickout = session.interactions[step]
    current_timestamp = current_clickout.timestamp # shorthand I'll use this a lot lot
    current_price = current_clickout.prices[choice_idx]
    current_choice_id = current_clickout.impressions[choice_idx]

    prev_clickouts: List[Interaction] = [o for o in session.interactions[:step] if o.is_clickout]
    # last_clickout is really useful for extracting features
    # -- this will be set to None if there was no clickout
    last_clickout = prev_clickouts[-1] if len(prev_clickouts) else None

    item_exists: bool
    try:
        id_to_hotel[current_choice_id]
        item_exists = True
    except KeyError:
        item_exists = False
        #print("Key Error... {} does not exist in our dict".format(current_choice_id))

    user_exists: bool = True
    if session.user_id not in users.keys():
        user_exists = False

    hotel_sims = [hotel_sim(current_choice_id, o.action_on) for o in session.interactions[:step] if o.action_on.isnumeric()]
    hotel_sims_embed = [hotel_sim_embed(current_choice_id, o.action_on) for o in prev_clickouts] if item_exists else [0]

    user_hotel_unique_sims = [hotel_sim(current_choice_id, hotel_id) for hotel_id in users[session.user_id].unique_interactions] if user_exists else [0]
    user_hotel_unique_sims_embed = [hotel_sim_embed(current_choice_id, hotel_id) for hotel_id in users[session.user_id].unique_interactions] if user_exists else [0]

    dst_shortest_path: float
    try:
        if nx.has_path(G,current_choice_id, session.user_id):
            dst_shortest_path = sigmoid(nx.shortest_path_length(G,current_choice_id, session.user_id, weight = None))
        else:
            dst_shortest_path = sigmoid(100.0)
    except nx.NodeNotFound:
        dst_shortest_path = -1.0

    features: Dict[str,Any] = { #type:ignore
        # session-based features
        "time_since_start": current_timestamp - session.start,
        "time_since_last_clickout": current_timestamp - last_clickout.timestamp if last_clickout else 0,
        "diff_price_mean": current_price - safe_mean(session.interactions[step].prices),
        "last_price_diff": current_price - last_clickout.prices[last_clickout.get_clicked_idx()] if last_clickout else 0,
        "reciprocal_choice_rank": 1 / (choice_idx + 1), # rank starts at 1 index starts at
        # person clicks 1 then 4... maybe they will try 7 next
        "predicted_next_click": 2 * last_clickout.get_clicked_idx() - prev_clickouts[-2].get_clicked_idx() + 1 if len(prev_clickouts) > 1 else last_clickout.get_clicked_idx() + 2 if last_clickout else 1,
        # position to previous clickout position
        "delta_position_last_position": (choice_idx) - last_clickout.get_clicked_idx() if last_clickout else 0,
        # z-score (?) difference between price and average price of clicked hotels
        "avg_price_sim": current_price - safe_mean([o.prices[o.get_clicked_idx()] for o in prev_clickouts]) if last_clickout else 0,
        "prev_impression_sim": jaccard(set(last_clickout.impressions),set(current_clickout.impressions)) if last_clickout else 0,
        # user-item or item-item features --
        # roll through the previous items interacted in the session and average their jaccard similarity to the current item
        "item_item_sim": safe_mean(hotel_sims),
        "item_item_embed_sim": safe_mean(hotel_sims_embed),
        "item_ctr": id_to_hotel[current_choice_id].ctr if item_exists else 0,
        "item_ctr_prob": id_to_hotel[current_choice_id].ctr_prob if item_exists else 0,
        # user-based features (these build on previous sessions or in conjunction with current sessions) --
        "unique_item_interact_by_user_avg": safe_mean(user_hotel_unique_sims),
        "unique_item_interact_by_user_embed_avg": safe_mean(user_hotel_unique_sims_embed),
        "unique_item_interact_by_user_max": max(user_hotel_unique_sims),
        "unique_item_interact_by_user_min": min(user_hotel_unique_sims),
        "path_user_item": dst_shortest_path
    }
    return features

def build_graph(session_ids: List[str]) -> nx.Graph:
    """
    Build a graph for collaborative filtering using a list of session ids.
    """
    G = nx.Graph()

    sessions: List[Session] = [sids_to_data[s_id] for s_id in session_ids]
    for s in sessions:
        clickouts = [o for o in s.interactions if o.is_clickout]
        G.add_edges_from([(o.action_on, s.user_id) for o in clickouts])

    return G

def make_w2v(session_ids: List[str]) -> Word2Vec:
    sessions: List[Session] = [sids_to_data[s_id] for s_id in session_ids]
    sequences: List[str] = []
    for s in sessions:
        clickouts = [o for o in s.interactions if o.is_clickout]
        sequences += [o.impressions for o in clickouts]

    n_cores = multiprocessing.cpu_count()

    # 1. initialise model with params
    w2v_model = Word2Vec(min_count=1,
                     window=4,
                     vector_size=100,
                     sample=6e-5,
                     alpha=0.03,
                     min_alpha=0.0007,
                     negative=8,
                     workers=n_cores-1)

    # 2. create the vocabulary
    w2v_model.build_vocab(sequences, progress_per=10000)

    t = time()
    w2v_model.train(sequences, total_examples=w2v_model.corpus_count, epochs=30, report_delay=1)
    print('Time to train the model: {} mins'.format(round((time() - t) / 60, 2)))

    return w2v_model

def collect(what: str, session_ids: List[str]) -> SessionData:
    """
    This function takes as input a filename and will return a SessionData object containing
    a list of clickout features, a list of labels, and methods to convert each into matrices.
    """
    sessions: List[Session] = [sids_to_data[s_id] for s_id in session_ids]

    print("Rolling through ({}) sessions/creating labeled feature vectors for {} sessions".format(len(sessions), what))

    examples: List[Dict[str,str]] = []
    qids: List[str] = []

    features: Dict[str,Any]

    for s in tqdm(sessions): # for each session -- --
        for step, o in enumerate(s.interactions): # for each interaction in the session

            # if it's of type "clickout", e.g. o.is_clickout
            if o.is_clickout:
                if o.get_clicked_idx() != -1:
                    # create a positive training example and 24 negatives... also extract features for each and add to x
                    for index, choice in enumerate(o.impressions):
                        label = (choice == o.action_on)

                        # feature extraction needs session, interaction info, and the index we were wondering about.
                        # we also pass whether we want to ignore user features. (defaults to false)
                        features = extract_features(s,step,index)
                        features["is_advantaged_user"] = 0 if s.user_id not in users.keys() else 1 if len(users[s.user_id].sessions) > 2 else 0
                        q_id = "{}/{}".format(s.session_id, step)
                        qids.append(q_id)
                        examples.append({"q_id": q_id, "choice_idx":index, **features, "y":label})

                    G.add_edge(o.action_on, s.user_id) # then add the edge to the graph.. aka build it in real time.

            # try to add each interaction to a user profile
            uid = s.user_id # (use this many times)
            user: UserProfile
            try:
                user = users[uid]
            except KeyError:
                # but if it doesn't work, create a new user at that address.
                user = UserProfile(uid)
                users[uid]= user
            user.update(s.session_id, o)

    feature_names: List[str] = [i for i in features.keys()]
    return SessionData(pd.DataFrame.from_records(examples), qids, feature_names)_____no_output_____def load_session_dict(dfpath: str) -> Dict[str,Session]:
    """
    This function be called on both train and test sets, so that I can get both a list of unique session ids to collect later,
    and a way to access these sessions (in collect) in  O(1) time.
    """

    sessions: List[Session] = []
    # nrows=1_000 for my laptop's sake
    df_interactions = pd.read_parquet(dfpath)
    # apply the "save_session" function to each grouped item/session
    # but first turn each group from a df into a list of dictionaries
    A = lambda x: sessions.append(create_session(x.to_dict("records"))) #type:ignore
    df_interactions.groupby(by="session_id").apply(A)

    sessions_dict: Dict[str,Session] = {}
    for s in sessions:
        sessions_dict[s.session_id] = s
    return sessions_dict_____no_output_____# globals
id_to_hotel: Dict[str,Hotel] = {}
users: Dict[str,UserProfile] = {} # this map ids to UserProfile objects (which are just sets of sessions)
# create an empty graph - nodes and edges will be built in real-time because we want to prevent crazy amounts of overfitting
G = nx.Graph()_____no_output_____

Generate features, splits, and quickly-loadable matrix files. There are two tasks:
1. make a parquet file for user-aware train/eval later
2. make another file to be used for a user-blind recommender. This is a little tricky because I still need to know if a user belongs to a 'advantaged' or 'disadvantaged' group. Pass in a parameter to     feature extraction to ignore the user ?_____no_output_____

print("Loading Session Data")
sessions_train = load_session_dict('/content/tempdata/train.parquet.gzip')
gc.collect()
sessions_vali = load_session_dict('/content/tempdata/validation.parquet.gzip')
gc.collect()
sessions_test = load_session_dict('/content/tempdata/confirmation.parquet.gzip')
gc.collect()

session_ids_train = list(sessions_train.keys())
session_ids_vali = list(sessions_vali.keys())
session_ids_test = list(sessions_test.keys())

sids_to_data = {**sessions_train, **sessions_vali,**sessions_test}Loading Session Data
print("Loading up item features")
hotel_features_df = pd.read_parquet('./data/silver/item_metadata_dense.parquet.gzip') #type:ignore
hotel_features_df["item_id"] = hotel_features_df["item_id"].apply(str) # make sure the id is of type str, not int
hotel_features_df["properties"] = hotel_features_df["properties"].str.split("|").map(set)
d: Dict = hotel_features_df.to_dict("records")
for h in d: # loop over the dictionary version of this df
    id_to_hotel[h["item_id"]] = Hotel(**h)
hotel_features_df.head()Loading up item features
# get w2v
w2v_model_path = './model/query_sim_w2v.model'
if not path.exists(w2v_model_path):
    make_w2v(session_ids_train).save(w2v_model_path)
w2v_model = Word2Vec.load(w2v_model_path)_____no_output_____train = collect("train",session_ids_train)
session_ids_vali_test = session_ids_test + session_ids_vali
session_ids_vali_test.sort(key=lambda x: sids_to_data[x].start)
valitest = collect("vali/test", session_ids_vali_test)
#test = collect("test", session_ids_test)

train.data["grp"] = 0
vali_or_test = lambda x: 2 if x.split("/")[0] in session_ids_test else 1
valitest.data["grp"] = valitest.data["q_id"].apply(vali_or_test)
#test.data["grp"]  = 2

frames: List[pd.DataFrame] = [train.data, valitest.data]
df_out = pd.concat(frames)
print("Writing a df with pyarrow. It has dimensions {}. ".format(df_out.shape))

# dump dataset and put my experiments in a different file
df_out.to_parquet("data/trivago/data_all.parquet", engine="pyarrow")_____no_output_____


# Notebook from RicardoSantos0/introtodeeplearning
Path: lab1/Part1_TensorFlow.ipynb

      Visit MIT Deep Learning

        Run in Google Colab

        View Source on GitHub

# Copyright Information
_____no_output_____

# Copyright 2021 MIT 6.S191 Introduction to Deep Learning. All Rights Reserved.
#
# Licensed under the MIT License. You may not use this file except in compliance
# with the License. Use and/or modification of this code outside of 6.S191 must
# reference:
#
# © MIT 6.S191: Introduction to Deep Learning
# http://introtodeeplearning.com
#_____no_output_____

# Lab 1: Intro to TensorFlow and Music Generation with RNNs

In this lab, you'll get exposure to using TensorFlow and learn how it can be used for solving deep learning tasks. Go through the code and run each cell. Along the way, you'll encounter several ***TODO*** blocks -- follow the instructions to fill them out before running those cells and continuing.

# Part 1: Intro to TensorFlow

## 0.1 Install TensorFlow

TensorFlow is a software library extensively used in machine learning. Here we'll learn how computations are represented and how to define a simple neural network in TensorFlow. For all the labs in 6.S191 2021, we'll be using the latest version of TensorFlow, TensorFlow 2, which affords great flexibility and the ability to imperatively execute operations, just like in Python. You'll notice that TensorFlow 2 is quite similar to Python in its syntax and imperative execution. Let's install TensorFlow and a couple of dependencies.
_____no_output_____

# magic command unique to google colab%tensorflow_version 2.x
import tensorflow as tf

# Download and import the MIT 6.S191 package
!pip install mitdeeplearning
import mitdeeplearning as mdl

import numpy as np
import matplotlib.pyplot as pltRequirement already satisfied: mitdeeplearning in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (0.2.0)
Requirement already satisfied: regex in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from mitdeeplearning) (2020.11.13)
Requirement already satisfied: numpy in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from mitdeeplearning) (1.19.1)
Requirement already satisfied: gym in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from mitdeeplearning) (0.18.0)
Requirement already satisfied: tensorflow>=2.0.0a in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from mitdeeplearning) (2.4.1)
Requirement already satisfied: tqdm in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from mitdeeplearning) (4.50.2)
Requirement already satisfied: pyglet<=1.5.0,>=1.4.0 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from gym->mitdeeplearning) (1.5.0)
Requirement already satisfied: scipy in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from gym->mitdeeplearning) (1.5.0)
Requirement already satisfied: Pillow<=7.2.0 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from gym->mitdeeplearning) (7.2.0)
Requirement already satisfied: cloudpickle<1.7.0,>=1.2.0 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from gym->mitdeeplearning) (1.6.0)
Requirement already satisfied: astunparse~=1.6.3 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (1.6.3)
Requirement already satisfied: opt-einsum~=3.3.0 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (3.3.0)
Requirement already satisfied: six~=1.15.0 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (1.15.0)
Requirement already satisfied: h5py~=2.10.0 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (2.10.0)
Requirement already satisfied: termcolor~=1.1.0 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (1.1.0)
Requirement already satisfied: absl-py~=0.10 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (0.11.0)
Requirement already satisfied: tensorboard~=2.4 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (2.4.1)
Requirement already satisfied: keras-preprocessing~=1.1.2 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (1.1.2)
Requirement already satisfied: protobuf>=3.9.2 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (3.14.0)
Requirement already satisfied: wrapt~=1.12.1 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (1.12.1)
Requirement already satisfied: tensorflow-estimator<2.5.0,>=2.4.0 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (2.4.0)
Requirement already satisfied: google-pasta~=0.2 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (0.2.0)
Requirement already satisfied: grpcio~=1.32.0 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (1.32.0)
Requirement already satisfied: typing-extensions~=3.7.4 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (3.7.4.3)
Requirement already satisfied: flatbuffers~=1.12.0 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (1.12)
Requirement already satisfied: wheel~=0.35 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (0.35.1)
Requirement already satisfied: gast==0.3.3 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorflow>=2.0.0a->mitdeeplearning) (0.3.3)
Requirement already satisfied: future in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from pyglet<=1.5.0,>=1.4.0->gym->mitdeeplearning) (0.18.2)
Requirement already satisfied: tensorboard-plugin-wit>=1.6.0 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (1.8.0)
Requirement already satisfied: google-auth-oauthlib<0.5,>=0.4.1 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (0.4.2)
Requirement already satisfied: requests<3,>=2.21.0 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (2.24.0)
Requirement already satisfied: setuptools>=41.0.0 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (49.6.0.post20200814)
Requirement already satisfied: google-auth<2,>=1.6.3 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (1.26.1)
Requirement already satisfied: werkzeug>=0.11.15 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (1.0.1)
Requirement already satisfied: markdown>=2.6.8 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (3.3.3)
Requirement already satisfied: requests-oauthlib>=0.7.0 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from google-auth-oauthlib<0.5,>=0.4.1->tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (1.3.0)
Requirement already satisfied: idna<3,>=2.5 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from requests<3,>=2.21.0->tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (2.10)
Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from requests<3,>=2.21.0->tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (1.25.10)
Requirement already satisfied: certifi>=2017.4.17 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from requests<3,>=2.21.0->tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (2020.12.5)
Requirement already satisfied: chardet<4,>=3.0.2 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from requests<3,>=2.21.0->tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (3.0.4)
Requirement already satisfied: pyasn1-modules>=0.2.1 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from google-auth<2,>=1.6.3->tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (0.2.8)
Requirement already satisfied: cachetools<5.0,>=2.0.0 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from google-auth<2,>=1.6.3->tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (4.2.1)
Requirement already satisfied: rsa<5,>=3.1.4; python_version >= "3.6" in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from google-auth<2,>=1.6.3->tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (4.7.1)
Requirement already satisfied: oauthlib>=3.0.0 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from requests-oauthlib>=0.7.0->google-auth-oauthlib<0.5,>=0.4.1->tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (3.1.0)
Requirement already satisfied: pyasn1<0.5.0,>=0.4.6 in c:\users\ricardo santos\anaconda3\envs\deep_\lib\site-packages (from pyasn1-modules>=0.2.1->google-auth<2,>=1.6.3->tensorboard~=2.4->tensorflow>=2.0.0a->mitdeeplearning) (0.4.8)

## 1.1 Why is TensorFlow called TensorFlow?

TensorFlow is called 'TensorFlow' because it handles the flow (node/mathematical operation) of Tensors, which are data structures that you can think of as multi-dimensional arrays. Tensors are represented as n-dimensional arrays of base dataypes such as a string or integer -- they provide a way to generalize vectors and matrices to higher dimensions.

The ```shape``` of a Tensor defines its number of dimensions and the size of each dimension. The ```rank``` of a Tensor provides the number of dimensions (n-dimensions) -- you can also think of this as the Tensor's order or degree.

Let's first look at 0-d Tensors, of which a scalar is an example:_____no_output_____

sport = tf.constant("Tennis", tf.string)
number = tf.constant(1.41421356237, tf.float64)

print("`sport` is a {}-d Tensor".format(tf.rank(sport).numpy()))
print("`number` is a {}-d Tensor".format(tf.rank(number).numpy()))`sport` is a 0-d Tensor
`number` is a 0-d Tensor

Vectors and lists can be used to create 1-d Tensors:_____no_output_____

sports = tf.constant(["Tennis", "Basketball"], tf.string)
numbers = tf.constant([3.141592, 1.414213, 2.71821], tf.float64)

print("`sports` is a {}-d Tensor with shape: {}".format(tf.rank(sports).numpy(), tf.shape(sports)))
print("`numbers` is a {}-d Tensor with shape: {}".format(tf.rank(numbers).numpy(), tf.shape(numbers)))`sports` is a 1-d Tensor with shape: [2]
`numbers` is a 1-d Tensor with shape: [3]

Next we consider creating 2-d (i.e., matrices) and higher-rank Tensors. For examples, in future labs involving image processing and computer vision, we will use 4-d Tensors. Here the dimensions correspond to the number of example images in our batch, image height, image width, and the number of color channels._____no_output_____

### Defining higher-order Tensors ###

'''TODO: Define a 2-d Tensor'''
#start by assigning 2d tensors
matrix = tf.constant([[1, 2], [3, 4], [5, 6], [2,0]], dtype=tf.float64)

#as nothing happens, the assertion is correct
assert isinstance(matrix, tf.Tensor), "matrix must be a tf Tensor object"
assert tf.rank(matrix).numpy() == 2_____no_output_____matrix_____no_output_____'''TODO: Define a 4-d Tensor.'''
# Use tf.zeros to initialize a 4-d Tensor of zeros with size 10 x 256 x 256 x 3.
#   You can think of this as 10 images where each image is RGB 256 x 256.
images = tf.zeros([10, 256, 256, 3], dtype = tf.int32)

assert isinstance(images, tf.Tensor), "matrix must be a tf Tensor object"
assert tf.rank(images).numpy() == 4, "matrix must be of rank 4"
assert tf.shape(images).numpy().tolist() == [10, 256, 256, 3], "matrix is incorrect shape"_____no_output_____

As you have seen, the ```shape``` of a Tensor provides the number of elements in each Tensor dimension. The ```shape``` is quite useful, and we'll use it often. You can also use slicing to access subtensors within a higher-rank Tensor:_____no_output_____

row_vector = matrix[1]
column_vector = matrix[:,1]
scalar = matrix[0, 1]

print("`row_vector`: {}".format(row_vector.numpy()))
print("`column_vector`: {}".format(column_vector.numpy()))
print("`scalar`: {}".format(scalar.numpy()))`row_vector`: [3. 4.]
`column_vector`: [2. 4. 6. 0.]
`scalar`: 2.0

## 1.2 Computations on Tensors

A convenient way to think about and visualize computations in TensorFlow is in terms of graphs. We can define this graph in terms of Tensors, which hold data, and the mathematical operations that act on these Tensors in some order. Let's look at a simple example, and define this computation using TensorFlow:

![alt text](https://raw.githubusercontent.com/aamini/introtodeeplearning/master/lab1/img/add-graph.png)_____no_output_____

# Create the nodes in the graph, and initialize values
a = tf.constant(15)
b = tf.constant(61)

# Add them!
c1 = tf.add(a,b)
c2 = a + b # TensorFlow overrides the "+" operation so that it is able to act on Tensors
print(c1)
print(c2)tf.Tensor(76, shape=(), dtype=int32)
tf.Tensor(76, shape=(), dtype=int32)

Notice how we've created a computation graph consisting of TensorFlow operations, and how  the output is a Tensor with value 76 -- we've just created a computation graph consisting of operations, and it's executed them and given us back the result.

Now let's consider a slightly more complicated example:

![alt text](https://raw.githubusercontent.com/aamini/introtodeeplearning/master/lab1/img/computation-graph.png)

Here, we take two inputs, `a, b`, and compute an output `e`. Each node in the graph represents an operation that takes some input, does some computation, and passes its output to another node.

Let's define a simple function in TensorFlow to construct this computation function:_____no_output_____

### Defining Tensor computations ###

# Construct a simple computation function
def func(a,b):
    '''performs a certain number of operations'''
    c = tf.add(a,b)
    d = tf.subtract(b, -1)
    e = tf.multiply(c, d)
    return e_____no_output_____

Now, we can call this function to execute the computation graph given some inputs `a,b`:_____no_output_____

# Consider example values for a,b
a, b = 1.5, 2.5
# Execute the computation
e_out = func(a,b)
print(e_out)tf.Tensor(14.0, shape=(), dtype=float32)

Notice how our output is a Tensor with value defined by the output of the computation, and that the output has no shape as it is a single scalar value._____no_output_____## 1.3 Neural networks in TensorFlow
We can also define neural networks in TensorFlow. TensorFlow uses a high-level API called [Keras](https://www.tensorflow.org/guide/keras) that provides a powerful, intuitive framework for building and training deep learning models.

Let's first consider the example of a simple perceptron defined by just one dense layer: $ y = \sigma(Wx + b)$, where $W$ represents a matrix of weights, $b$ is a bias, $x$ is the input, $\sigma$ is the sigmoid activation function, and $y$ is the output. We can also visualize this operation using a graph:

![alt text](https://raw.githubusercontent.com/aamini/introtodeeplearning/master/lab1/img/computation-graph-2.png)

Tensors can flow through abstract types called [```Layers```](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Layer) -- the building blocks of neural networks. ```Layers``` implement common neural networks operations, and are used to update weights, compute losses, and define inter-layer connectivity. We will first define a ```Layer``` to implement the simple perceptron defined above._____no_output_____

### Defining a network Layer ###

# n_output_nodes: number of output nodes
# input_shape: shape of the input
# x: input to the layer

class OurDenseLayer(tf.keras.layers.Layer):
    def __init__(self, n_output_nodes):
        super(OurDenseLayer, self).__init__()
        self.n_output_nodes = n_output_nodes

    def build(self, input_shape):
        d = int(input_shape[-1])
        # Define and initialize parameters: a weight matrix W and bias b
        # Note that parameter initialization is random!
        self.W = self.add_weight("weight", shape=[d, self.n_output_nodes]) # note the dimensionality
        self.b = self.add_weight("bias", shape=[1, self.n_output_nodes]) # note the dimensionality

    def call(self, x):
        '''makes the linear transformation of z = Wx + b (hint: use tf.matmul)'''
        z = tf.matmul(x, self.W) + self.b

        '''TODO: define the operation for out (hint: use tf.sigmoid)'''
        y = tf.sigmoid(z)
        return y

# Since layer parameters are initialized randomly, we will set a random seed for reproducibility
tf.random.set_seed(1)
layer = OurDenseLayer(3)
layer.build((1,2))
x_input = tf.constant([[1,2.]], shape=(1,2))
y = layer.call(x_input)

# test the output!
print(y.numpy())
mdl.lab1.test_custom_dense_layer_output(y)[[0.26978594 0.45750415 0.66536945]]
[PASS] test_custom_dense_layer_output

Conveniently, TensorFlow has defined a number of ```Layers``` that are commonly used in neural networks, for example a [```Dense```](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Dense?version=stable). Now, instead of using a single ```Layer``` to define our simple neural network, we'll use the  [`Sequential`](https://www.tensorflow.org/versions/r2.0/api_docs/python/tf/keras/Sequential) model from Keras and a single [`Dense` ](https://www.tensorflow.org/versions/r2.0/api_docs/python/tf/keras/layers/Dense) layer to define our network. With the `Sequential` API, you can readily create neural networks by stacking together layers like building blocks. _____no_output_____

### Defining a neural network using the Sequential API ###

# Import relevant packages
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense

# Define the number of outputs
n_output_nodes = 3

# First define the model
model = Sequential()

'''TODO: Define a dense (fully connected) layer to compute z'''
# Remember: dense layers are defined by the parameters W and b!
# You can read more about the initialization of W and b in the TF documentation :)
# https://www.tensorflow.org/api_docs/python/tf/keras/layers/Dense?version=stable
dense_layer = tf.keras.layers.Dense(n_output_nodes, activation='sigmoid')

# Add the dense layer to the model
model.add(dense_layer)
_____no_output_____

That's it! We've defined our model using the Sequential API. Now, we can test it out using an example input:_____no_output_____

# Test model with example input
x_input = tf.constant([[1,2.]], shape=(1,2))

'''feed input into the model and predict the output!'''
model_output =  model(x_input).numpy()
print(model_output)[[0.6504886  0.4782816  0.83736604]]

In addition to defining models using the `Sequential` API, we can also define neural networks by directly subclassing the [`Model`](https://www.tensorflow.org/api_docs/python/tf/keras/Model?version=stable) class, which groups layers together to enable model training and inference. The `Model` class captures what we refer to as a "model" or as a "network". Using Subclassing, we can create a class for our model, and then define the forward pass through the network using the `call` function. Subclassing affords the flexibility to define custom layers, custom training loops, custom activation functions, and custom models. Let's define the same neural network as above now using Subclassing rather than the `Sequential` model._____no_output_____

### Defining a model using subclassing ###

from tensorflow.keras import Model
from tensorflow.keras.layers import Dense

class SubclassModel(tf.keras.Model):

  # In __init__, we define the Model's layers
  def __init__(self, n_output_nodes):
    super(SubclassModel, self).__init__()
    '''TODO: Our model consists of a single Dense layer. Define this layer.'''
    #we define a dense layers with n_output_nodes
    self.dense_layer = Dense(n_output_nodes, activation='sigmoid')

  # In the call function, we define the Model's forward pass.
  def call(self, inputs):
    return self.dense_layer(inputs)_____no_output_____

Just like the model we built using the `Sequential` API, let's test out our `SubclassModel` using an example input.

_____no_output_____

n_output_nodes = 3
model = SubclassModel(n_output_nodes)

x_input = tf.constant([[1,2.]], shape=(1,2))

print(model.call(x_input))tf.Tensor([[0.29996255 0.62776643 0.48460072]], shape=(1, 3), dtype=float32)

Importantly, Subclassing affords us a lot of flexibility to define custom models. For example, we can use boolean arguments in the `call` function to specify different network behaviors, for example different behaviors during training and inference. Let's suppose under some instances we want our network to simply output the input, without any perturbation. We define a boolean argument `isidentity` to control this behavior:_____no_output_____

### Defining a model using subclassing and specifying custom behavior ###

from tensorflow.keras import Model
from tensorflow.keras.layers import Dense

class IdentityModel(tf.keras.Model):

  # As before, in __init__ we define the Model's layers
  # Since our desired behavior involves the forward pass, this part is unchanged
  def __init__(self, n_output_nodes):
    super(IdentityModel, self).__init__()
    self.dense_layer = tf.keras.layers.Dense(n_output_nodes, activation='sigmoid')

  '''TODO: Implement the behavior where the network outputs the input, unchanged,
      under control of the isidentity argument.'''
  def call(self, inputs, isidentity=False):
    x = self.dense_layer(inputs)
    #do not apply anything if the inputs are identity
    if isidentity:
        return inputs
    return x_____no_output_____

Let's test this behavior:_____no_output_____

n_output_nodes = 3
model = IdentityModel(n_output_nodes)

x_input = tf.constant([[1,2.]], shape=(1,2))
'''TODO: pass the input into the model and call with and without the input identity option.'''
out_activate = model.call(x_input) #uses x_input as input to output
out_identity = model.call(x_input, isidentity=True)
print("Network output with activation: {}; network identity output: {}".format(out_activate.numpy(), out_identity.numpy()))Network output with activation: [[0.1969584 0.6330007 0.7668015]]; network identity output: [[1. 2.]]

Now that we have learned how to define `Layers` as well as neural networks in TensorFlow using both the `Sequential` and Subclassing APIs, we're ready to turn our attention to how to actually implement network training with backpropagation._____no_output_____## 1.4 Automatic differentiation in TensorFlow

[Automatic differentiation](https://en.wikipedia.org/wiki/Automatic_differentiation)
is one of the most important parts of TensorFlow and is the backbone of training with
[backpropagation](https://en.wikipedia.org/wiki/Backpropagation). We will use the TensorFlow GradientTape [`tf.GradientTape`](https://www.tensorflow.org/api_docs/python/tf/GradientTape?version=stable) to trace operations for computing gradients later.

When a forward pass is made through the network, all forward-pass operations get recorded to a "tape"; then, to compute the gradient, the tape is played backwards. By default, the tape is discarded after it is played backwards; this means that a particular `tf.GradientTape` can only
compute one gradient, and subsequent calls throw a runtime error. However, we can compute multiple gradients over the same computation by creating a ```persistent``` gradient tape.

First, we will look at how we can compute gradients using GradientTape and access them for computation. We define the simple function $ y = x^2$ and compute the gradient:_____no_output_____

### Gradient computation with GradientTape ###

# y = x^2
# Example: x = 3.0
x = tf.Variable(3.0)

# Initiate the gradient tape
with tf.GradientTape() as tape:
  # Define the function
  y = x * x
# Access the gradient -- derivative of y with respect to x
dy_dx = tape.gradient(y, x)

assert dy_dx.numpy() == 6.0_____no_output_____

In training neural networks, we use differentiation and stochastic gradient descent (SGD) to optimize a loss function. Now that we have a sense of how `GradientTape` can be used to compute and access derivatives, we will look at an example where we use automatic differentiation and SGD to find the minimum of $L=(x-x_f)^2$. Here $x_f$ is a variable for a desired value we are trying to optimize for; $L$ represents a loss that we are trying to  minimize. While we can clearly solve this problem analytically ($x_{min}=x_f$), considering how we can compute this using `GradientTape` sets us up nicely for future labs where we use gradient descent to optimize entire neural network losses._____no_output_____

### Function minimization with automatic differentiation and SGD ###

# Initialize a random value for our initial x
x = tf.Variable([tf.random.normal([1])])
print("Initializing x={}".format(x.numpy()))

learning_rate = 1e-2 # learning rate for SGD
history = []
# Define the target value
x_f = 4

# We will run SGD for a number of iterations. At each iteration, we compute the loss,
#   compute the derivative of the loss with respect to x, and perform the SGD update.
for i in range(500):
  with tf.GradientTape() as tape:
    '''TODO: define the loss as described above'''
    loss = loss = (x - x_f)**2

  # loss minimization using gradient tape
  grad = tape.gradient(loss, x) # compute the derivative of the loss with respect to x
  new_x = x - learning_rate*grad # sgd update
  x.assign(new_x) # update the value of x
  history.append(x.numpy()[0])

# Plot the evolution of x as we optimize towards x_f!
plt.plot(history)
plt.plot([0, 500],[x_f,x_f])
plt.legend(('Predicted', 'True'))
plt.xlabel('Iteration')
plt.ylabel('x value')Initializing x=[[-0.00839665]]

`GradientTape` provides an extremely flexible framework for automatic differentiation. In order to back propagate errors through a neural network, we track forward passes on the Tape, use this information to determine the gradients, and then use these gradients for optimization using SGD._____no_output_____


# Notebook from SalishSeaCast/analysis-karyn
Path: notebooks/plotExamples/SaanichChl&Zoop.ipynb

import numpy as np  # this module handles arrays, but here we need it for its NaN value
import pandas as pd # this module contains a lot of tools for handling tabular data
from matplotlib import pyplot as plt
from salishsea_tools import evaltools as et
import datetime as dt
import os
import gsw
import netCDF4 as nc
import cmocean
from scipy import stats as spst
from pandas.plotting import register_matplotlib_converters
register_matplotlib_converters()
%matplotlib inline_____no_output_____

## Load data and matched model output_____no_output_____

dfchl=pd.read_excel('/ocean/ksuchy/MOAD/observe/SaanichChlorophyll.xlsx',engine='openpyxl')_____no_output_____dfzoop=pd.read_excel('/ocean/ksuchy/MOAD/observe/SaanichGrowthRates.xlsx',engine='openpyxl')_____no_output_____modSourceDir= '/results2/SalishSea/nowcast-green.201905/'
modver='201905'
Chl_N=1.8 # Chl:N ratio
fname='SaanichChlorophyll.ipynb'
startYMD=(2010,1,1)
endYMD=(2011,12,31)_____no_output_____# Parameters
modSourceDir = "/results2/SalishSea/nowcast-green.201905/"
modver = "201905"
Chl_N = 1.8
fname = "SaanichChlorophyll.ipynb"
_____no_output_____start_date = dt.datetime(startYMD[0],startYMD[1],startYMD[2])
end_date = dt.datetime(endYMD[0],endYMD[1],endYMD[2]) #dt.datetime(2019,6,30)_____no_output_____datestr='_'+start_date.strftime('%Y%m%d')+'_'+end_date.strftime('%Y%m%d')_____no_output_____with nc.Dataset('/ocean/eolson/MEOPAR/NEMO-forcing/grid/mesh_mask201702_noLPE.nc') as mesh:
    tmask=np.copy(mesh.variables['tmask'][0,:,:,:])
    navlat=np.copy(mesh.variables['nav_lat'][:,:])
    navlon=np.copy(mesh.variables['nav_lon'][:,:])_____no_output_____PATH= modSourceDir_____no_output_____flen=1 # number of days per model output file. always 1 for 201905 and 201812 model runs
namfmt='nowcast' # for 201905 and 201812 model runs, this should always be 'nowcast'
# filemap is dictionary of the form variableName: fileType, where variableName is the name
# of the variable you want to extract and fileType designates the type of
# model output file it can be found in (usually ptrc_T for biology, grid_T for temperature and
# salinity)
filemap={'diatoms':'ptrc_T','ciliates':'ptrc_T','flagellates':'ptrc_T','microzooplankton':'ptrc_T','mesozooplankton':'ptrc_T'}
# fdict is a dictionary mappy file type to its time resolution. Here, 1 means hourly output
# (1h file) and 24 means daily output (1d file). In certain runs, multiple time resolutions
# are available
fdict={'ptrc_T':1,'grid_T':1} #24 for hours averaged _____no_output_____data=et.matchData(dfchl,filemap,fdict,start_date,end_date,namfmt,PATH,flen,quiet=False,method='vertNet')_____no_output_____data_____no_output_____data.keys()_____no_output_____def yd(idt):
    if type(idt)==dt.datetime:
        yd=(idt-dt.datetime(idt.year-1,12,31)).days
    else: # assume array or pandas
        yd=[(ii-dt.datetime(ii.year-1,12,31)).days for ii in idt]
    return yd

data['yd']=yd(data['dtUTC'])
data['Year']=[ii.year for ii in data['dtUTC']]_____no_output_____# define log transform function with slight shift to accommodate zero values
def logt(x):
    return np.log10(x+.001)_____no_output_____

### Total Chlorophyll Comparisons_____no_output_____

# total chlorophyll comparisons

fig,ax=plt.subplots(1,2,figsize=(12,5))
ax[0].plot(data['Total Chl (mg m-3)'],Chl_N*(data['mod_flagellates']+data['mod_ciliates']+data['mod_diatoms']),'k.')
ax[0].set_title('Total Chlorophyll (mg m-3)')
ax[0].set_xlabel('In situ')
ax[0].set_ylabel('model')
ax[0].plot((0,20),(0,20),'r-',alpha=.3)

ax[1].plot(logt(data['Total Chl (mg m-3)']),logt(Chl_N*(data['mod_flagellates']+data['mod_ciliates']+data['mod_diatoms'])),'k.')
ax[1].set_title('log10[ Total Chlorophyll (mg m-3) + 0.001]')
ax[1].set_xlabel('In situ')
ax[1].set_ylabel('model')
ax[1].plot((-6,5),(-6,5),'r-',alpha=.3)
ax[1].set_xlim(-2,2)
ax[1].set_ylim(-2,2);_____no_output_____

#### By time of year_____no_output_____

fig,ax=plt.subplots(1,1,figsize=(5,5))
m=ax.scatter(logt(data['Total Chl (mg m-3)']),logt(Chl_N*(data['mod_flagellates']+data['mod_ciliates']+data['mod_diatoms'])),
          c=data['yd'],s=5,cmap=cmocean.cm.phase)
ax.set_title('log10[ Total Chlorophyll (mg m-3) + 0.001] By Year Day')
ax.set_xlabel('In situ')
ax.set_ylabel('model')
ax.plot((-6,5),(-6,5),'r-',alpha=.3)
ax.set_xlim(-2,2)
ax.set_ylim(-2,2);
fig.colorbar(m)_____no_output_____data2=et.matchData(dfzoop,filemap,fdict,start_date,end_date,namfmt,PATH,flen,quiet=False,method='vertNet')_____no_output_____data2_____no_output_____data2.keys()_____no_output_____

### Total Zooplankton Comparisons_____no_output_____

# total zooplankton comparisons

fig,ax=plt.subplots(1,2,figsize=(12,5))
ax[0].plot(data2['Total Zoop Biomass'],(data2['mod_mesozooplankton']*5.7*12),'k.')
ax[0].set_title('Total Zooplankton (mg C m-3)')
ax[0].set_xlabel('In situ')
ax[0].set_ylabel('Model')
ax[0].plot((0,20),(0,20),'r-',alpha=.3)

ax[1].plot(logt(data2['Total Zoop Biomass']),logt((data2['mod_mesozooplankton']*5.7*12)),'k.')
ax[1].set_title('log10[ Total Zooplankton (mg C m-3) + 0.001]')
ax[1].set_xlabel('In situ')
ax[1].set_ylabel('Model')
ax[1].plot((-6,5),(-6,5),'r-',alpha=.3)
ax[1].set_xlim(0,4)
ax[1].set_ylim(0,4);

fig,ax=plt.subplots(1,2,figsize=(12,5))
ax[0].plot(data2['Crustacean Biomass'],(data2['mod_mesozooplankton']*5.7*12),'k.')
ax[0].set_title('Total Crustacean Biomass (mg C m-3)')
ax[0].set_xlabel('In situ')
ax[0].set_ylabel('Model')
ax[0].plot((0,100),(0,100),'r-',alpha=.3)

ax[1].plot(logt(data2['Crustacean Biomass']),logt((data2['mod_mesozooplankton']*5.7*12)),'k.')
ax[1].set_title('log10[ Total Crustacean Biomass (mg C m-3) + 0.001]')
ax[1].set_xlabel('In situ')
ax[1].set_ylabel('Model')
ax[1].plot((-6,5),(-6,5),'r-',alpha=.3)
ax[1].set_xlim(0,4)
ax[1].set_ylim(0,4);

fig,ax=plt.subplots(1,2,figsize=(12,5))
ax[0].plot(data2['Non Crustacean Biomass'],(data2['mod_mesozooplankton']*5.7*12),'k.')
ax[0].set_title('Total Non-Crustacean Biomass (mg C m-3)')
ax[0].set_xlabel('In situ')
ax[0].set_ylabel('Model')
ax[0].plot((0,100),(0,100),'r-',alpha=.3)

ax[1].plot(logt(data2['Non Crustacean Biomass']),logt((data2['mod_mesozooplankton']*5.7*12)),'k.')
ax[1].set_title('log10[ Total Non-Crustacean Biomass (mg C m-3) + 0.001]')
ax[1].set_xlabel('In situ')
ax[1].set_ylabel('Model')
ax[1].plot((-6,5),(-6,5),'r-',alpha=.3)
ax[1].set_xlim(0,4)
ax[1].set_ylim(0,4);

fig,ax=plt.subplots(1,2,figsize=(12,5))
ax[0].plot(data2['Copepod Biomass'],(data2['mod_mesozooplankton']*5.7*12),'k.')
ax[0].set_title('Total Copepod Biomass (mg C m-3)')
ax[0].set_xlabel('In situ')
ax[0].set_ylabel('Model')
ax[0].plot((0,100),(0,100),'r-',alpha=.3)

ax[1].plot(logt(data2['Copepod Biomass']),logt((data2['mod_mesozooplankton']*5.7*12)),'k.')
ax[1].set_title('log10[ Total Copepod Biomass (mg C m-3) + 0.001]')
ax[1].set_xlabel('In situ')
ax[1].set_ylabel('Model')
ax[1].plot((-6,5),(-6,5),'r-',alpha=.3)
ax[1].set_xlim(0,4)
ax[1].set_ylim(0,4);_____no_output_____


# Notebook from Priyanshusinhaa/Quantum-Computing
Path: 2021-africa/lab-2/lab2.ipynb

# Quantum speedups in finance

### Notebook objectives:

In this challenge we will gain the following skills:
1. Understand the basics of how financial instruments are typically priced using Monte Carlo methods
2. Implement a quantum algorithm to price a financial instrument (in this case, we consider derivative contracts called options)
3. Understand the pros/cons of using the quantum approach_____no_output_____## Introduction

The world of finance is a complicated one to model and predict. The financial markets, in particular, are influenced by so many external factors and contain multiple different investors with different objectives. This means that traditional modelling techniques, like multiple linear regression, often struggle when trying to explain investors' behaviours or perform certain tasks in finance, such as predicting the price of a stock. Due to these complexities, we need more sophisticated models and numerous techniques have been proposed to produce such robust models for financial instruments like stocks.

In Africa, the dynamics of the financial markets are arguably even more complicated. In this notebook, we discuss what types of financial instruments we can encounter in the South African market, particularly those that are liquid (meaning they are frequently traded and accurately valued). Illiqid instruments are also prevalent in Africa and are more difficult to evaluate and price, since they are not often traded.

Here, we introduce a common pricing technique for a particular instrument called an option. This technique rests on Monte Carlo sampling which we show can achieve a quadratic speed up in pricing the option by replacing the classical Monte Carlo sampling approach with a quantum algorithm that leverages quantum effects to compute things faster. _____no_output_____## 1. What is a financial instrument?

A financial instrument is any asset that can be traded between various parties. For example, shares of a company are equity instruments, and debt, cash and even contracts can be considered as financial instruments.

In general, we can divide financial instruments into several categories, such as cash instruments, asset or debt-based instruments and derivative instruments. In this challenge, we will focus on the last category, namely, derivatives. These instruments get their name from the fact that their price is *derived* based on the price of a separate underlying quantity. In particular, we will consider derivatives called European options. _____no_output__________no_output_____### Option contracts

Options are financial derivatives that are defined explicitly by contracts. These contracts give the buyer of the option the right, but not the obligation, to buy or sell a specific underlying asset at an agreed-upon price sometime in the future.

- Options that give the buyer the right to *buy* the underlying asset are called **call options**

- Options that give the buyer the right to *sell* the underlying asset are called **put options**.

In both cases, the price to buy or sell the asset at, is agreed upon in the contract and called the **strike price**.

For example, let's assume that the share price of a company called ABC is currently trading at $S_0 =$ 50 ZAR. You believe that in 1 months time, the share price will double to 100 ZAR. You can either buy the share now for 50 ZAR, or you could buy a 1-month call option for a much cheaper price of 5 ZAR. If the price of the ABC share does indeed double in 1 months time, then you can exercise your option right to buy the ABC share at the agreed up strike price (which will be lower than the actual share price). This may sound a little tricky, so let's make it concrete by going through an example with various scenarios we can encounter.

Recall that the current price of ABC is $S_0 =$ 50 ZAR. A 1-month call option with a strike price of $K =$ 80 ZAR is available to purchase for $P_{\mathrm{call}} =$ 5 ZAR. Let's consider the scenarios of buying an ABC share today or the call option.

#### Today:

Scenario 1: Buying a share of ABC today
Investment = $S_0 =$ 50 ZAR

Scenario 2: Buying a 1-month call option on ABC
Investment = $P_{\mathrm{call}} =$ 5 ZAR

#### In 1-months time:
Since option contracts are valid for a pre-determined period of time, their value at the expiration date is called the *payoff* that you will recieve. Technically, the price of an ABC share could be trading at any value greater than or equal to 0 ZAR. Let's imagine that the price after 1-month $S_t$ can be 40 ZAR, 60 ZAR or 100 ZAR and look at the payoffs for each scenario.

- $S_t =$ 40 ZAR \
    Scenario 1: Since you own the share of ABC and purchased it at 50 ZAR, you will lose 10 ZAR. I.e. the payoff will be a loss of -10 ZAR. \
    Scenario 2: The strike price is higher than the actual share price. Thus, you would not execute the option to buy ABC at $K =$ 80 ZAR when you can purchase the share on the market at 40 ZAR. You will lose out on the price you paid for the call option and so the payoff will be a loss of -5 ZAR.

- $S_t =$ 60 ZAR \
Scenario 1: The payoff will be the profit you made from buying the share at 50 ZAR and it's current price. I.e., payoff = 60 - 50 = 10 ZAR. \
Scenario 2: The strike price is still higher than the share price. Thus, you would not execute the option and lose 5 ZAR.

- $S_t =$ 100 ZAR \
Scenario 1: payoff = 100 - 50 = 50 ZAR \
Scenario 2: Here, the strike price is lower than the market price so it makes sense to execute the option. By doing so, you can buy the share at the strike price of 80 ZAR and the profit is therefore = 100 - 80 = 20 ZAR. However, remember that you also paid a premium for the option so the total payoff is 100 - 80 - 5 = 15 ZAR.

At this stage, you might be wondering why buy the option at all? And it kind of seems like betting? What's important to keep in mind is the initial investment you have to put up in order to buy the actual ABC share vs buying the call option. Buying the share requires a much higher investment and you run the risk of losing a lot more money if the share price drops below the price you initially paid for it. Whereas with the option, if the share price drops below the strike price, you can just let the option expire and lose a maximum of 5 ZAR (the price you paid for the option). There are also other reasons to purchase options, such as hedging against risk or offsetting other trades. In general, you can read more about these strategies here: https://www.investopedia.com/trading/options-strategies/_____no_output_____# Pricing options

Now, how on earth do we begin to price these option contracts? Previously, we said the price of a 1-month call option linked to the ABC share price was 5 ZAR. But is that price fair? And how would be go about determining the fair price of these options in general?

If we think about what should affect the fair price of an option, it boils down to understanding the what the price of the underlying asset it is linked to will be in the future. Due to the random (also called stochastic) nature of most of the parameters that go into pricing the underlying assets that options are defined on, calculating the fair price can be a difficult task, and whilst analytical models exist for the basic types of options, the simplifying assumptions required for these models often limit their applicability. Thus, numerical methods that estimate the fair price have to be employed for option pricing, with Monte Carlo being one of the most popular. _____no_output_____## 2. How does Monte Carlo work?

Monte Carlo methods are a class of computational algorithms that are based on random sampling and repeated computation. At a high level, imagine you have a function (or equation) that is described by multiple variables and you're interested in solving that function, but it is analytically difficult to do so because the function depends on lots of complicated variables.

A Monte Carlo approach simply samples random values for the parameters of your function from an underlying distribution and computes the function multiple times, each time using a different set of randomly sampled values. In doing so, we can obtain an expected value for the function we are trying to evaluate by taking an average over all the computed values of the function. This allows us to estimate function values without having to analytically solve them directly._____no_output_____

You can read more about how Monte Carlo works in this blog post that discusses how to use the technique to estimate the value of pi: https://medium.com/swlh/estimate-pi-with-monte-carlo-a74995862501_____no_output_____### Monte Carlo in finance

Monte Carlo simulation is often used in finance to value and analyse instruments, portfolios and investments by simulating the sources of uncertainty that affect their value [cite: https://www.cmi.ac.in/~shariq/Shariq%20files/option_pricing.pdf].

In the case of options, Monte Carlo methods are used to develop a price distribution for the underlying asset. If we have a price distribution of the underlying asset, then we can begin to get a sense of what the fair price for the option should be.

To illustrate this, we have to first make assumptions about the factors that could influence the price of the underlying asset and hence, influence the value of the option._____no_output_____### Monte Carlo methods for option pricing

The goal of option pricing is to estimate the option's payoff at the expiration date. In other words, how much profit can you expect to make from the option? If the underlying asset's price is expected to go up a lot, then you can expect to make more money if you can buy the underlying asset at a cheap price when it is worth a lot. In that case, the option price should be high since the option to buy the underlying asset at a cheaper price is quite valuable.

The steps to price an option are as follows:

1. Model the price of the underlying asset which the option is based on, and any other sources of uncertainty, as random variables $\textbf{X}=\{X_1, X_2, . . . , X_N\} $ which follow a stochastic process.
$$ \thinspace $$
2. Generate a large number, $M$, of random values which can serve as price paths $\{\textbf{X}_1, \textbf{X}_2, . . . ,  \textbf{X}_N\} $ for the underlying asset. These random values should be drawn from the probability distribution implied by the stochastic process. Let's call this distribution $\mathbb{P}$.
$$ \thinspace $$
3. Once we have lots of simulated price paths for the underlying asset, we can calculate the option’s payoff for each of the generated price paths, which we can label as $f(\textbf{X}_i)$. Then we can compute an estimator for the expectation value of the payoff as an average across all paths, i.e. $\mathbb{E}_\mathbb{P}[f(\textbf{X})]$ can be approximated by
$$ \mathbb{\hat{E}}_\mathbb{P}[f(\textbf{X})]= \frac{1}{M}\sum_{i=1}^M f(\textbf{X}_i) $$
$$ \thinspace $$
4. Lastly, discount the calculated expectation value to get the option's fair value today.

In step 4, the discounting process requires knowledge of interest rates at future dates which is itself an important question from a financial modelling perspective. However, for the types of options we consider, this process is not computationally challenging and can be performed classically after the payoff calculation. We therefore do not discount the expected payoff for simplicity and can ignore step 4.

Let's look at a simple example to illustrate how classical Monte Carlo works using Python._____no_output_____

# Let's import some libraries and functions we will need

import math
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline_____no_output_____

Let's look at a 1-month call option linked to the underlying share price of a new company called QuantumTech. We can formulate the expected payoff in 1 months time, and hence the price of the call option, as follows:

$$P_{\mathrm{call}} = \max(S_T - K, 0)$$

where $S_T$ is the price of the QuantumTech share in $T =$ 1 month and $K$ is the pre-agreed upon strike price.

Since we know $K$ and $T$, we need to estimate $S_T$. I.e. we need to use Monte Carlo methods to estimate QuantumTech's share price in 1 month. But what function can sample from in order to get the Monte Carlo estimate?_____no_output_____In financial literature, there are several results that help us answer this question. Share prices are often assumed to follow a random process called a Generalised Wiener Process. We can use this random process to create lots of estimates for the price of a QuantumTech share. Let's assume we expect QuantumTech to have roughly a positive return for the month of $r = 0.05$ and there will be some volatility around this return which we denote as $\sigma = 0.4$. Then, the change in share price, $dS_T$, can be written as

$$ dS_T = rS_TdT + \sigma S_T dW_T $$

where $dW_T$ is a Wiener process (see https://www.whoi.edu/fileserver.do?id=21268&pt=10&p=17232 for more technical details of this process). Using a convenient result called Itô's lemma, we can then derive the formula for the share price at time T as

$$S_T = S_0e^{(r-\frac{1}{2}\sigma^2)T + \sigma \sqrt{T}W_T}$$

where $S_0$ is the price of a QuantumTech share today. Thus, often shares are modelled using log-normal distributions.

Now we can code up this function and use lots of random values to evaluate it, in order to calculate a price distribution for QuantumTech in 1 months time._____no_output_____

# Monte Carlo valuation of a European call option

# set a random seed to reproduce our results
np.random.seed(42)

# set the parameters
S0 = 141 # initial price of the underlying asset
K = 177 # strike price
r = 0.05 # average return of the underlying
sigma = 0.4 # volatility of the underlying
T = 1 # time till execution
t = 30 # number of time steps we want to divide T in
dt = T / t # incremental time step size
M = 1000 # number of paths to simulate

# Simulating M price paths with t time steps
# sum instead of cumsum would also do if only the final values at end of the month (i.e. at time T) are of interest
S = S0 * np.exp(np.cumsum((r - 0.5 * sigma ** 2) * dt + sigma * math.sqrt(dt)* np.random.standard_normal((t + 1, M)), axis=0))

# Calculating the Monte Carlo estimator for the expected payoff
P_call = sum(np.maximum(S[-1] - K, 0)) / M

# Results output
print("The call option value is:  {:0.2f} ZAR.\n".format(P_call))The call option value is:  7.64 ZAR.

Let's visualise the multiple paths that the underlying share price of QuantumTech could take. The resulting call option value is calculated using an average of these paths._____no_output_____

num_paths_to_plot = 100
plt.figure(figsize= (12,8))
plt.plot(S[:, :num_paths_to_plot])
plt.grid(True)
plt.xlabel('days')
plt.ylabel('Price')
plt.title('Possible price paths for a QuantumTech share')
plt.show()_____no_output_____

Let’s investigate the frequency of the simulated index levels at the end of the simulation period_____no_output_____

plt.figure(figsize= (10,5))
plt.hist(S[-1], bins=50)
plt.grid(True)
plt.xlabel('Price')
plt.ylabel('frequency')_____no_output_____

Let’s look at the histogram of all simulated end-of-period option values_____no_output_____

plt.figure(figsize= (10,5))
plt.hist(np.maximum(S[-1] - K, 0), bins=50)
plt.grid(True)
plt.xlabel('option payoff value')
plt.ylabel('frequency')_____no_output_____

## 3. The quantum approach

Traditional Monte Carlo methods generally require extensive computational resources. By leveraging the laws of quantum mechanics, a quantum computer may provide novel ways to solve computationally intensive problems.

Quantitative finance may benefit from quantum computing in many ways. Recently developed applications of gate-based quantum computing algorithms for use in finance include portfolio optimisation, the calculation of risk measures and pricing derivatives. Several of these applications are based on an algorithm called Amplitude Estimation, which can estimate a parameter's value with a convergence rate in the order of $\mathcal{O}(M^{-1})$, where $M$ is the number of samples required. This represents a theoretical quadratic speed-up compared to Monte Carlo methods that run on classical computers with a convergence rate of $\mathcal{O}(M^{-1/2})$.

Below is a graphical demonstration of this speedup taken from a paper called Option Pricing Using Quantum Computers (https://arxiv.org/abs/1905.02666). The y-axis depicts the estimation error of the function or parameter we are trying to model, versus the number of samples we use, $M$, to create the estimate. _____no_output__________no_output_____### Pricing options with Quantum Amplitude Estimation

If we were to swap out the Monte Carlo estimate for the underlying share price and use Amplitude Estimation, the building blocks needed to price the option on a gate-based quantum computer are the following:

1) Represent the probability distribution $ \mathbb{P} $ describing the evolution of the share price of QuantumTech on a quantum computer.
$$ \thinspace $$
2) Construct the quantum model which computes the payoff of the option, $f(\textbf{X})$.
$$ \thinspace $$
3) Calculate the expectation value of the payoff $\mathbb{E}_\mathbb{P}[f(\textbf{X})]$.

In the paper Quantum Risk Analysis (https://arxiv.org/abs/1806.06893) you can find a detailed description of how to use Amplitude Estimation to calculate the expectation value of a function of random variables, how to load a relevant probability distribution to a quantum register and the construction of the quantum circuits needed to compute the payoff and set up Amplitude Estimation to estimate the expectation value of the payoff. There is also a chapter in the qiskit textbook explaining Amplitude Estimation in more detail._____no_output_____Continuing with our example of a 1-month European call option based on the share price of QuantumTech $S_T$ and a strike price $K$, recall that the corresponding payoff function is defined as:

$$f(S_T)=\max(S_T - K, 0).$$

We now know that the price of this type of option depends only on the distribution of $S_T$. In the rest of this notebook, we will use a quantum algorithm that employs Amplitude Estimation to estimate the expected payoff, i.e., the fair price for the option:

$$\mathbb{E}\left[ \max(S_T - K, 0) \right]$$_____no_output_____

#First we import all the required libraries
from qiskit import Aer
from qiskit.utils import QuantumInstance
from qiskit.algorithms import IterativeAmplitudeEstimation
from qiskit_finance.circuit.library import LogNormalDistribution, EuropeanCallPricingObjective_____no_output_____

### Recapping our quantum understanding

Quantum models consist of quantum circuits that contain operations which rotate qubits in different ways. Depending on the types of rotations used, these will influence the outcome of the quantum circuit when we measure each qubit. From the preliminary notebook, we know that when a qubit is measured, it collapses to a classical outcome of either 0 or 1. But, we must also remember that the outcome of quantum measurements when qubits are in superposition is stochastic, meaning, we must measure the circuit several times to get an accurate distribution over the possible outcomes. These possible outcomes are what we call basis states, and each of them have corresponding probability amplitudes which tell us how probable the basis states are of being measured. For example, if we 2 qubits, the basis states are $00, 01, 10$ and $11$ - each of which will have a probability associated with it.
_____no_output__________no_output_____### Distribution Loading
Returning to our QuantumTech example, the first component of our option pricing model is to create a quantum circuit that takes in the probability distribution implied for the possible share prices of QuantumTech in 1 months time and loads it into a register such that each basis state represents a possible share value and its amplitude, the corresponding probability of the share having that value.

In this notebook, we construct a circuit factory to load a log-normal probability distribution for the share price of QuantumTech into a quantum state. The share price distribution is truncated to a given interval $[low, high]$ and discretised using $2^n$ grid points, where $n$ denotes the number of qubits used. This might sound a bit confusing, so feel free to check out this blog post [INSERT LINK] for more details on loading distributions as quantum states.

Essentially we want to represent a probability distribution of our underlying share price as a quantum state. Thus, we reduce the the distribution to an interval we deem relevant and "slice" the possible values in between this interval. The number of slices we make will depend on how many qubits we have. If we have $n$ qubits, this means we have $2^n$ equally spaced slices in the interval. Each slice corresponds to a certain share price which we can then associate with a possible basis state. Thus, the more qubits we have, the more possible basis states we will have and the more slices we can create. Let's look at an example.

### The quantum uncertainty model_____no_output_____

# number of qubits to represent the uncertainty/distribution
num_uncertainty_qubits = 2

# parameters for considered random distribution
S = 50       # initial spot price
strike_price = 55
vol = 0.4     # volatility of 40%
r = 0.05      # annual interest rate of 5%
T = 30 / 365  # 30 days to maturity

# resulting parameters for log-normal distribution
mu = ((r - 0.5 * vol**2) * T + np.log(S))
sigma = vol * np.sqrt(T)
mean = np.exp(mu + sigma**2/2)
variance = (np.exp(sigma**2) - 1) * np.exp(2*mu + sigma**2)
stddev = np.sqrt(variance)

# lowest and highest value considered for the spot price; in between, an equidistant discretization is considered.
# we truncate the distribution to the interval defined by 2 standard deviations around the mean
low  = np.maximum(0, mean - 2*stddev)
high = mean + 2*stddev

# construct circuit factory for uncertainty model
uncertainty_model = LogNormalDistribution(num_uncertainty_qubits, mu=mu, sigma=sigma**2, bounds=(low, high))_____no_output_____# plot probability distribution
x = uncertainty_model.values
y = uncertainty_model.probabilities
plt.figure(figsize= (10,5))
plt.bar(x, y, width=1)
plt.xticks(x, size=15, rotation=90)
plt.yticks(size=15)
plt.grid()
plt.xlabel('Share Price in one month $S_T$ (\$)', size=15)
plt.ylabel('Probability ($\%$)', size=15)
plt.show()_____no_output_____

### Payoff Function

Let's have a look at the payoff function for our QuantumTech option. Recall that the payoff function equals zero as long as the share price in 1 months time, $S_T$, is less than the strike price $K$ and then increases linearly thereafter. The code below illustrates this._____no_output_____

# plot exact payoff function (evaluated on the grid of the uncertainty model)
x = uncertainty_model.values
y = np.maximum(0, x - strike_price)
plt.figure(figsize= (10,5))
plt.plot(x, y, 'ro-')
plt.grid()
plt.title('Normalised Payoff Function', size=15)
plt.xlabel('Share Price', size=15)
plt.ylabel('Normalised Payoff', size=15)
plt.xticks(x, size=15, rotation=90)
plt.yticks(size=15)
plt.show()_____no_output_____

### Evaluate the expected payoff

Lastly, we can use Quantum Amplitude Estimation to compute the expected payoff of the option. Thanks to built-in Qiskit algorithms, we can use the EuropeanCallPricing and IterativeAmplitudeEstimation functions to achieve this._____no_output_____

from qiskit_finance.applications.estimation import EuropeanCallPricing

european_call_pricing = EuropeanCallPricing(num_state_qubits=num_uncertainty_qubits,
                                            strike_price=strike_price,
                                            rescaling_factor=0.25,
                                            bounds=(low, high),
                                            uncertainty_model=uncertainty_model)_____no_output_____# set target precision and confidence level
epsilon = 0.055
alpha = 0.00000001
shots = 100
simulator = 'qasm_simulator'

qi = QuantumInstance(Aer.get_backend(simulator), shots=shots, seed_simulator=42, seed_transpiler=42)
problem = european_call_pricing.to_estimation_problem()

# construct amplitude estimation
ae = IterativeAmplitudeEstimation(epsilon, alpha=alpha, quantum_instance=qi)
result = ae.estimate(problem)_____no_output_____

Since this example is simple enough to calculate by hand, we can compute the exact expected value of the option and compare it to the result of Amplitude Estimation using a quantum approach.

We can also create a confidence interval around our quantum solution. Recall that we are modelling probability distributions which are inherently random. Thus, if we want, we can instead provide a range of values for our option price instead of just one fixed value.

Below is the exact value, followed by the estimated value of the option using Amplitude Estimation. The estimation error is simply the difference between the exact and estimated values. And finally, the confidence interval provides a range of values which the model predicts the final output to lie, based on the value of alpha. _____no_output_____

# evaluate exact expected value (normalized to the [0, 1] interval)
exact_value = np.dot(uncertainty_model.probabilities, y)

conf_int = np.array(result.confidence_interval_processed)
print('Exact value:        \t%.4f' % exact_value)
print('Estimated value:    \t%.4f' % (european_call_pricing.interpret(result)))
print('Estimation error:    \t%.4f' %(np.abs(exact_value-european_call_pricing.interpret(result))))
print('Confidence interval:\t[%.4f, %.4f]' % tuple(conf_int))Exact value:        	0.5199
Estimated value:    	0.5191
Estimation error:    	0.0009
Confidence interval:	[-0.1476, 1.1857]

Ideally, we would want our model's estimated value to be as close as possible to the exact value. Thus, we would want to minimise the estimation error as much as possible.

Now that we have implemented the quantum approach to price a call option, we can start to attempt the exercises below to enhance and create more models._____no_output_____# Exercises_____no_output_____### Exercise 2a

The estimated value for the expected payoff can be made more accurate. Can you make the estimated value closer to the exact value by tweaking various parameters? Try reducing the estimation error as much as possible and input your final values below.

NOTE: Do not change any other variables than the ones specified below._____no_output_____

# Run this cell to get a score for your answer, copy the values for the variables here

# HINT: Try lower the estimation error to below 0.03

num_uncertainty_qubits = 2
low  = np.maximum(0, mean - 2*stddev)
high = mean + 2*stddev
epsilon = 0.055
alpha = 0.00000001
shots = 100
simulator = 'qasm_simulator'

# Run this cell once you are ready to submit your answer

from qc_grader import grade_ex2a

solutions = [num_uncertainty_qubits, low, high, epsilon, alpha, shots, simulator]

grade_ex2a(solutions)Submitting your answer for ex2/partA. Please wait...
Congratulations 🎉! Your answer is correct and has been submitted.
Your score is 100.

 In this exercise, your score is calculated based on the estimation error of your model. The lower the estimation error, the higher your score. The highest possible score is 100._____no_output_____### Exercise 2b

Recall that we can have different types of options. What we have implemented so far in this notebook is a European call option. In this exercise, try to implement a **European put option**. A put option gives the buyer of the option the right (but not the obligation) to sell the underlying asset, rather than to buy it. Thus, a put option has a completely different payoff function to a call option.

Let's assume the following **fixed** parameters to implement a put option:

```
num_uncertainty_qubits = 4
S = 200 # initial spot price
vol = 0.3 # volatility of 30%
r = 0.08 # annual interest rate of 8%
T = 60 / 365 # 60 days to maturity
strike_price = 230
epsilon = 0.01
alpha = 0.05
shots = 100
simulator = 'qasm_simulator'
````_____no_output_____Below is some code to get you started.

HINT: if you are stuck, check out this qiskit tutorial and adapt it for our problem: https://qiskit.org/documentation/finance/tutorials/04_european_put_option_pricing.html._____no_output_____

# Do not change these variables

num_uncertainty_qubits = 4
S = 200 # initial spot price
vol = 0.3 # volatility of 30%
r = 0.08 # annual interest rate of 4%
T = 60 / 365 # 60 days to maturity
strike_price = 230
epsilon = 0.01
alpha = 0.05
shots = 100
simulator = 'qasm_simulator'

# set the approximation scaling for the payoff function
rescaling_factor = 0.25

# resulting parameters for log-normal distribution
mu = ((r - 0.5 * vol**2) * T + np.log(S))
sigma = vol * np.sqrt(T)
mean = np.exp(mu + sigma**2/2)
variance = (np.exp(sigma**2) - 1) * np.exp(2*mu + sigma**2)
stddev = np.sqrt(variance)
low  = np.maximum(0, mean - 2*stddev)
high = mean + 2*stddev
breakpoints = [low, high]
slopes = [-1, 0]
offsets = [strike_price - low, 0]
f_min = 0
f_max = strike_price - low_____no_output_____# The distribution loading step will be the same for the underlying asset, in this case the QuantumTech share

uncertainty_model = LogNormalDistribution(num_uncertainty_qubits, mu=mu, sigma=sigma**2, bounds=(low, high))
_____no_output_____from qiskit.circuit.library import  LinearAmplitudeFunction

# setup piecewise linear objective function, the LinearAmplitudeFunction

european_put_objective = LinearAmplitudeFunction(
    num_uncertainty_qubits,
    slopes,
    offsets,
    domain=(low, high),
    image=(f_min, f_max),
    breakpoints=breakpoints,
    rescaling_factor=rescaling_factor,
)_____no_output_____# setup the quantum instance to pass to the IterativeAmplitudeEstimation function

qi = QuantumInstance(Aer.get_backend(simulator), shots=shots, seed_simulator=42, seed_transpiler=42)

# construct amplitude estimation

ae = IterativeAmplitudeEstimation(epsilon, alpha=alpha, quantum_instance=qi)_____no_output_____# Run this cell to get a score for your answer!

from qc_grader import grade_ex2b

grade_ex2b(uncertainty_model, european_put_objective, ae)Submitting your answer for ex2/partB. Please wait...
Congratulations 🎉! Your answer is correct and has been submitted.

## Advanced reading (optional)

### Computing the delta of an option_____no_output_____The delta of an option measures the risk or the sensitivity of an option's change in price if the underlying asset's price changes. It is option quoted as a simple ration of the change in price of the underlying asset to the change in price of the option:

$\mathrm{delta} = \frac{\Delta S}{\Delta P}$, where $S$ is the underlying asset's price and $P$ is the price of the option. You can read more about the details of deltas for options here: https://www.investopedia.com/terms/d/delta.asp

In Qiskit, we can also conveniently compute the delta of an option. Continuing on with the parameters of our put option we just implemented, let's compute the delta._____no_output_____

from qiskit.algorithms import EstimationProblem_____no_output_____# setup a piecewise linear objective function
european_put_delta_objective = LinearAmplitudeFunction(
    num_uncertainty_qubits,
    slopes,
    offsets,
    domain=(low, high),
    image=(f_min, f_max),
    breakpoints=breakpoints
)

# construct circuits for payoff function
european_put_delta = european_put_delta_objective.compose(uncertainty_model, front=True)_____no_output_____# Set up the estimation problem using qiskit
problem = EstimationProblem(state_preparation=european_put_delta,
                            objective_qubits=[num_uncertainty_qubits])

# construct amplitude estimation
ae_delta = IterativeAmplitudeEstimation(epsilon, alpha=alpha, quantum_instance=qi)

# get the estimated result
result_delta = ae_delta.estimate(problem)

# compute the exact result
x = uncertainty_model.values
exact_delta = -sum(uncertainty_model.probabilities[x <= strike_price])

# compare results
conf_int = -np.array(result_delta.confidence_interval)[::-1]
print('Exact delta:    \t%.4f' % exact_delta)
print('Esimated value: \t%.4f' % -result_delta.estimation)
print('Estimation error:    \t%.4f' %(np.abs(exact_delta+result_delta.estimation)))
print('Confidence interval: \t[%.4f, %.4f]' % tuple(conf_int))Exact delta:    	-0.8783
Esimated value: 	-0.3750
Estimation error:    	0.5033
Confidence interval: 	[-0.3772, -0.3728]

# Conclusion

Now that you are familiar with how quantum algorithms can be used for financial applications, go ahead and start exploring the Qiskit Finance module and check out all the other types of quantum finance applications that are out there! You can even begin to modify the code and tailor them for problems in real life.

### Good luck! _____no_output_____

import qiskit.tools.jupyter
%qiskit_version_table
%qiskit_copyright/opt/conda/lib/python3.8/site-packages/qiskit/aqua/__init__.py:86: DeprecationWarning: The package qiskit.aqua is deprecated. It was moved/refactored to qiskit-terra For more information see <https://github.com/Qiskit/qiskit-aqua/blob/main/README.md#migration-guide>
  warn_package('aqua', 'qiskit-terra')