Skip to content

Instantly share code, notes, and snippets.

@jfeigl-ottogroup
Last active September 27, 2022 15:35
Show Gist options
  • Save jfeigl-ottogroup/79559d3f8105b389534dfc6876fddb33 to your computer and use it in GitHub Desktop.
Save jfeigl-ottogroup/79559d3f8105b389534dfc6876fddb33 to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "sitting-voluntary",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import pandas as pd\n",
"import torch\n",
"from sklearn.metrics import accuracy_score\n",
"from sklearn.preprocessing import LabelEncoder\n",
"from sklearn.utils import shuffle\n",
"from skorch import NeuralNetClassifier\n",
"from skorch.callbacks import GradientNormClipping, LRScheduler\n",
"from skorch.classifier import CVSplit, EpochScoring, NeuralNet, NeuralNetClassifier\n",
"from skorch.dataset import Dataset\n",
"from skorch.helper import SliceDict, predefined_split\n",
"from torch import nn\n",
"from torch.utils.data import DataLoader, IterableDataset"
]
},
{
"cell_type": "markdown",
"id": "stuffed-soundtrack",
"metadata": {},
"source": [
"# Create dummy data"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "original-complement",
"metadata": {},
"outputs": [],
"source": [
"df_purchases = pd.DataFrame(\n",
" {\n",
" \"customer_id\": [\"A\", \"A\", \"B\", \"B\", \"B\", \"C\", \"C\"],\n",
" \"product_id\": [\"X\", \"Y\", \"Z\", \"X\", \"Y\", \"Y\", \"Z\"],\n",
" \"shop_size\": [40, 42, 16, 44, 44, 40, 10],\n",
" }\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "determined-montreal",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>customer_id</th>\n",
" <th>product_id</th>\n",
" <th>shop_size</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>A</td>\n",
" <td>X</td>\n",
" <td>40</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>A</td>\n",
" <td>Y</td>\n",
" <td>42</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>B</td>\n",
" <td>Z</td>\n",
" <td>16</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>B</td>\n",
" <td>X</td>\n",
" <td>44</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>B</td>\n",
" <td>Y</td>\n",
" <td>44</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>C</td>\n",
" <td>Y</td>\n",
" <td>40</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>C</td>\n",
" <td>Z</td>\n",
" <td>10</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" customer_id product_id shop_size\n",
"0 A X 40\n",
"1 A Y 42\n",
"2 B Z 16\n",
"3 B X 44\n",
"4 B Y 44\n",
"5 C Y 40\n",
"6 C Z 10"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"df_purchases"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "bored-diameter",
"metadata": {},
"outputs": [],
"source": [
"# df with all possible products and sizes\n",
"df_product_size = pd.DataFrame(\n",
" {\n",
" \"product_id\": [\"X\", \"X\", \"X\", \"X\", \"Y\", \"Y\", \"Y\", \"Y\", \"Z\", \"Z\", \"Z\", \"Z\"],\n",
" \"shop_size\": [40, 42, 44, 46, 38, 40, 42, 44, 10, 12, 14, 16],\n",
" }\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "adjustable-christopher",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>product_id</th>\n",
" <th>shop_size</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>X</td>\n",
" <td>40</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>X</td>\n",
" <td>42</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>X</td>\n",
" <td>44</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>X</td>\n",
" <td>46</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>Y</td>\n",
" <td>38</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>Y</td>\n",
" <td>40</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>Y</td>\n",
" <td>42</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>Y</td>\n",
" <td>44</td>\n",
" </tr>\n",
" <tr>\n",
" <th>8</th>\n",
" <td>Z</td>\n",
" <td>10</td>\n",
" </tr>\n",
" <tr>\n",
" <th>9</th>\n",
" <td>Z</td>\n",
" <td>12</td>\n",
" </tr>\n",
" <tr>\n",
" <th>10</th>\n",
" <td>Z</td>\n",
" <td>14</td>\n",
" </tr>\n",
" <tr>\n",
" <th>11</th>\n",
" <td>Z</td>\n",
" <td>16</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" product_id shop_size\n",
"0 X 40\n",
"1 X 42\n",
"2 X 44\n",
"3 X 46\n",
"4 Y 38\n",
"5 Y 40\n",
"6 Y 42\n",
"7 Y 44\n",
"8 Z 10\n",
"9 Z 12\n",
"10 Z 14\n",
"11 Z 16"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"df_product_size"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "genuine-aluminum",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"LabelEncoder()"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# product-size tuples need to be encoded for easier use during training\n",
"product_size_encoder = LabelEncoder()\n",
"product_size_encoder.fit(\n",
" df_product_size[\"product_id\"] + \"__\" + df_product_size[\"shop_size\"].astype(str)\n",
")"
]
},
{
"cell_type": "markdown",
"id": "emotional-knife",
"metadata": {},
"source": [
"# Sample pairwise tuples"
]
},
{
"cell_type": "markdown",
"id": "orange-surname",
"metadata": {},
"source": [
"1. Sample a table with similar sized product-size-tuples (df_equal):\n",
" - Sampling two random products from each customer is done by shuffling the whole table and selecting the first and last item. This is much faster than a '.groupby.sample'-approach.\n",
" - The final table has only four columns: Two similar sized products (product_id_A and product_id_B) with their size (shop_size_A and shop_size_B)\n",
"\n",
"2. df_equal is then merged with all possible product-size-tuples (df_product_size) by product_id_B. We only keep the rows where size (from df_product_size) is one size larger than shop_size_B.\n",
" - This final table has two columns: product_shop_size_A and product_shop_size_B\n",
" - For these pairwise tuples, product_shop_size_B should always be larger than product_shop_size_A\n",
" \n",
"3. We repeat step 2 but only keep samples where product_shop_size_B is smaller than product_shop_size_A."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "sporting-teddy",
"metadata": {},
"outputs": [],
"source": [
"def get_sample(df):\n",
" df = df.sample(frac=1)\n",
"\n",
" df_equal = df.groupby(\"customer_id\").agg([\"first\", \"last\"]).reset_index()\n",
" df_equal.columns = [\n",
" \"customer_id\",\n",
" \"product_id_A\",\n",
" \"product_id_B\",\n",
" \"shop_size_A\",\n",
" \"shop_size_B\",\n",
" ]\n",
"\n",
" df_equal = df_equal.query(\"product_id_A != product_id_B\").drop(\n",
" columns=[\"customer_id\"]\n",
" )\n",
"\n",
" ####################\n",
" # positive samples #\n",
" ####################\n",
"\n",
" df_train = (\n",
" pd.merge(\n",
" df_equal, df_product_size, left_on=\"product_id_B\", right_on=\"product_id\"\n",
" )\n",
" .query(\"shop_size_B < shop_size\")\n",
" .drop(columns=[\"product_id_B\", \"shop_size_B\"])\n",
" .sort_values([\"product_id_A\", \"shop_size_A\", \"product_id\", \"shop_size\"])\n",
" .groupby([\"product_id_A\", \"shop_size_A\", \"product_id\"])\n",
" .first()\n",
" .reset_index()\n",
" )\n",
"\n",
" # product_size_A < product_size_B\n",
" df_train_pos = pd.DataFrame(\n",
" {\n",
" \"product_shop_size_A\": df_train.product_id_A\n",
" + \"__\"\n",
" + df_train.shop_size_A.astype(str),\n",
" \"product_shop_size_B\": df_train.product_id\n",
" + \"__\"\n",
" + df_train.shop_size.astype(str),\n",
" }\n",
" )\n",
"\n",
" y_train_pos = np.ones(len(df_train), dtype=np.float32)\n",
"\n",
" ####################\n",
" # negative samples #\n",
" ####################\n",
"\n",
" df_train = (\n",
" pd.merge(\n",
" df_equal, df_product_size, left_on=\"product_id_B\", right_on=\"product_id\"\n",
" )\n",
" .query(\"shop_size_B > shop_size\")\n",
" .drop(columns=[\"product_id_B\", \"shop_size_B\"])\n",
" .sort_values([\"product_id_A\", \"shop_size_A\", \"product_id\", \"shop_size\"])\n",
" .groupby([\"product_id_A\", \"shop_size_A\", \"product_id\"])\n",
" .last()\n",
" .reset_index()\n",
" )\n",
"\n",
" # product_size_A > product_size_B\n",
" df_train_neg = pd.DataFrame(\n",
" {\n",
" \"product_shop_size_A\": df_train.product_id_A\n",
" + \"__\"\n",
" + df_train.shop_size_A.astype(str),\n",
" \"product_shop_size_B\": df_train.product_id\n",
" + \"__\"\n",
" + df_train.shop_size.astype(str),\n",
" }\n",
" )\n",
"\n",
" y_train_neg = np.zeros(len(df_train), dtype=np.float32)\n",
"\n",
" ##################\n",
" # concat samples #\n",
" ##################\n",
"\n",
" # concat\n",
" df_train = pd.concat(\n",
" (df_train_pos.reset_index(drop=True), df_train_neg.reset_index(drop=True)),\n",
" 0,\n",
" ignore_index=True,\n",
" )\n",
" y_train = np.array(list(y_train_pos) + list(y_train_neg))\n",
"\n",
" # shuffle\n",
" df_train, y_train = shuffle(df_train, y_train)\n",
"\n",
" # encode\n",
" df_train.product_shop_size_A = product_size_encoder.transform(\n",
" df_train.product_shop_size_A\n",
" )\n",
" df_train.product_shop_size_B = product_size_encoder.transform(\n",
" df_train.product_shop_size_B\n",
" )\n",
"\n",
" yield df_train.values, y_train.reshape(-1, 1).astype(np.float32)"
]
},
{
"cell_type": "markdown",
"id": "downtown-framework",
"metadata": {},
"source": [
"# Train a model"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "surprising-certificate",
"metadata": {},
"outputs": [],
"source": [
"class RankingModel(nn.Module):\n",
" def __init__(self, product_size_count):\n",
" super().__init__()\n",
" self.product_size_embs = nn.Embedding(product_size_count, 1)\n",
" nn.init.constant_(self.product_size_embs.weight, 0)\n",
"\n",
" self.sig = nn.Sigmoid()\n",
"\n",
" def forward(self, X):\n",
" return self.sig(\n",
" self.product_size_embs(X[:, 1]) - self.product_size_embs(X[:, 0])\n",
" )"
]
},
{
"cell_type": "markdown",
"id": "unavailable-timeline",
"metadata": {},
"source": [
"#### Helper functions"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "representative-immigration",
"metadata": {},
"outputs": [],
"source": [
"class OurNeuralNet(NeuralNetClassifier):\n",
" def __init__(self, module, **kwargs):\n",
" super().__init__(module, **kwargs)\n",
"\n",
" def predict(self, X):\n",
" y_preds = self.predict_proba(X)\n",
" return (y_preds > 0.5).astype(np.float32)"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "olive-habitat",
"metadata": {},
"outputs": [],
"source": [
" class PairwiseIterDataset(IterableDataset):\n",
" def __init__(self, df):\n",
" self.df = df\n",
" def __iter__(self):\n",
" X, y = next(iter(get_sample(self.df)))\n",
"\n",
" return iter(zip(X, y))"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "continuous-stewart",
"metadata": {},
"outputs": [],
"source": [
"def custom_accuracy(net, X, y):\n",
" y_pred = net.predict(X)\n",
" return (y_pred == y).mean()"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "aggregate-surgery",
"metadata": {},
"outputs": [],
"source": [
"def is_monotonic_increasing(net, X, y):\n",
" # we are cheating a bit here by simply using df_product_size\n",
" df_result = df_product_size.copy()\n",
" df_result[\"size_universal\"] = (\n",
" net.module_.product_size_embs.weight.detach().numpy().flatten()\n",
" )\n",
" return (\n",
" df_result.sort_values([\"product_id\", \"shop_size\"])\n",
" .groupby([\"product_id\"])\n",
" .size_universal.apply(lambda x: x.is_monotonic_increasing)\n",
" .mean()\n",
" )"
]
},
{
"cell_type": "markdown",
"id": "pending-cardiff",
"metadata": {},
"source": [
"#### Start training"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "involved-ratio",
"metadata": {},
"outputs": [],
"source": [
"# get constant validation set\n",
"X_valid, y_valid = list(get_sample(df_purchases))[0]"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "biblical-above",
"metadata": {},
"outputs": [],
"source": [
"net = OurNeuralNet(\n",
" module=RankingModel,\n",
" module__product_size_count=len(product_size_encoder.classes_),\n",
" criterion=nn.BCELoss,\n",
" optimizer=torch.optim.Adam,\n",
" optimizer__lr=0.005,\n",
" optimizer__amsgrad=True,\n",
" max_epochs=15,\n",
" device=\"cpu\",\n",
" train_split=predefined_split(Dataset(X_valid, y_valid)),\n",
" batch_size=128,\n",
" callbacks=[\n",
" EpochScoring(custom_accuracy, name=\"acc\", lower_is_better=False, on_train=True),\n",
" EpochScoring(\n",
" is_monotonic_increasing,\n",
" name=\"monotonicity\",\n",
" lower_is_better=False,\n",
" on_train=False,\n",
" ),\n",
" ],\n",
")\n"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "suburban-jesus",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" epoch acc monotonicity train_loss valid_acc valid_loss dur\n",
"------- ------ -------------- ------------ ----------- ------------ ------\n",
" 1 \u001b[36m0.4000\u001b[0m \u001b[32m1.0000\u001b[0m \u001b[35m0.6931\u001b[0m \u001b[31m0.3333\u001b[0m \u001b[94m0.6931\u001b[0m 0.1545\n",
" 2 \u001b[36m0.5000\u001b[0m 0.3333 \u001b[35m0.6925\u001b[0m \u001b[31m0.6667\u001b[0m \u001b[94m0.6919\u001b[0m 0.0578\n",
" 3 \u001b[36m0.6667\u001b[0m 0.0000 \u001b[35m0.6907\u001b[0m \u001b[31m1.0000\u001b[0m \u001b[94m0.6895\u001b[0m 0.0706\n",
" 4 \u001b[36m0.8000\u001b[0m 0.0000 0.6919 1.0000 \u001b[94m0.6874\u001b[0m 0.0621\n",
" 5 0.7500 0.0000 \u001b[35m0.6884\u001b[0m 1.0000 \u001b[94m0.6848\u001b[0m 0.0708\n",
" 6 0.8000 0.0000 0.6892 1.0000 \u001b[94m0.6823\u001b[0m 0.0572\n",
" 7 \u001b[36m1.0000\u001b[0m 0.0000 \u001b[35m0.6862\u001b[0m 1.0000 \u001b[94m0.6793\u001b[0m 0.1124\n",
" 8 0.6667 0.3333 0.6895 1.0000 \u001b[94m0.6767\u001b[0m 0.0620\n",
" 9 1.0000 0.3333 0.6864 1.0000 \u001b[94m0.6742\u001b[0m 0.0717\n",
" 10 1.0000 0.6667 \u001b[35m0.6764\u001b[0m 1.0000 \u001b[94m0.6715\u001b[0m 0.0844\n",
" 11 1.0000 0.6667 \u001b[35m0.6688\u001b[0m 1.0000 \u001b[94m0.6685\u001b[0m 0.0611\n",
" 12 1.0000 0.6667 0.6800 1.0000 \u001b[94m0.6656\u001b[0m 0.0742\n",
" 13 1.0000 0.6667 0.6777 1.0000 \u001b[94m0.6629\u001b[0m 0.0551\n",
" 14 0.7500 0.6667 0.6750 1.0000 \u001b[94m0.6603\u001b[0m 0.0639\n",
" 15 0.7500 0.6667 0.6733 1.0000 \u001b[94m0.6576\u001b[0m 0.0598\n"
]
},
{
"data": {
"text/plain": [
"<class '__main__.OurNeuralNet'>[initialized](\n",
" module_=RankingModel(\n",
" (product_size_embs): Embedding(12, 1)\n",
" (sig): Sigmoid()\n",
" ),\n",
")"
]
},
"execution_count": 19,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"net.fit(PairwiseIterDataset(df_purchases), None)"
]
},
{
"cell_type": "markdown",
"id": "desperate-clinic",
"metadata": {},
"source": [
"# Extract universal size mapping table"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "endless-kuwait",
"metadata": {},
"outputs": [],
"source": [
"df_product_size[\"size_universal\"] = (\n",
" net.module_.product_size_embs.weight.detach().numpy().flatten()\n",
")\n"
]
},
{
"cell_type": "code",
"execution_count": 21,
"id": "large-finding",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>product_id</th>\n",
" <th>shop_size</th>\n",
" <th>size_universal</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>X</td>\n",
" <td>40</td>\n",
" <td>-0.004355</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>X</td>\n",
" <td>42</td>\n",
" <td>0.024786</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>X</td>\n",
" <td>44</td>\n",
" <td>0.039178</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>X</td>\n",
" <td>46</td>\n",
" <td>0.044262</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>Y</td>\n",
" <td>38</td>\n",
" <td>-0.055549</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>Y</td>\n",
" <td>40</td>\n",
" <td>-0.053047</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>Y</td>\n",
" <td>42</td>\n",
" <td>-0.035943</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>Y</td>\n",
" <td>44</td>\n",
" <td>0.046096</td>\n",
" </tr>\n",
" <tr>\n",
" <th>8</th>\n",
" <td>Z</td>\n",
" <td>10</td>\n",
" <td>-0.044564</td>\n",
" </tr>\n",
" <tr>\n",
" <th>9</th>\n",
" <td>Z</td>\n",
" <td>12</td>\n",
" <td>0.043662</td>\n",
" </tr>\n",
" <tr>\n",
" <th>10</th>\n",
" <td>Z</td>\n",
" <td>14</td>\n",
" <td>-0.020642</td>\n",
" </tr>\n",
" <tr>\n",
" <th>11</th>\n",
" <td>Z</td>\n",
" <td>16</td>\n",
" <td>0.035126</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" product_id shop_size size_universal\n",
"0 X 40 -0.004355\n",
"1 X 42 0.024786\n",
"2 X 44 0.039178\n",
"3 X 46 0.044262\n",
"4 Y 38 -0.055549\n",
"5 Y 40 -0.053047\n",
"6 Y 42 -0.035943\n",
"7 Y 44 0.046096\n",
"8 Z 10 -0.044564\n",
"9 Z 12 0.043662\n",
"10 Z 14 -0.020642\n",
"11 Z 16 0.035126"
]
},
"execution_count": 21,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"df_product_size"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "desirable-authorization",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "contextual5",
"language": "python",
"name": "contextual5"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment