{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<p align=\"center\">\n",
    "    <img src=\"https://github.com/GeostatsGuy/GeostatsPy/blob/master/TCG_color_logo.png?raw=true\" width=\"220\" height=\"240\" />\n",
    "\n",
    "</p>\n",
    "\n",
    "## Interactive Bootstrap Demonstration\n",
    "\n",
    "### Boostrap for Uncertainty in Sample Statistics Tutorial\n",
    "\n",
    "* interactive plot demonstration with ipywidget package\n",
    "\n",
    "#### Michael Pyrcz, Associate Professor, The University of Texas at Austin \n",
    "\n",
    "##### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1) | [GeostatsPy](https://github.com/GeostatsGuy/GeostatsPy)\n",
    "\n",
    "#### Bootstrap\n",
    "\n",
    "Uncertainty in the sample statistics\n",
    "* one source of uncertainty is the paucity of data.\n",
    "* do 200 or even less wells provide a precise (and accurate estimate) of the mean? standard deviation? skew? P13?\n",
    "\n",
    "Would it be useful to know the uncertainty in these statistics due to limited sampling?\n",
    "* what is the impact of uncertainty in the mean porosity e.g. 20%+/-2%?\n",
    "\n",
    "**Bootstrap** is a method to assess the uncertainty in a sample statistic by repeated random sampling with replacement.\n",
    "\n",
    "Assumptions\n",
    "* sufficient, representative sampling, identical, idependent samples\n",
    "\n",
    "Limitations\n",
    "1. assumes the samples are representative \n",
    "2. assumes stationarity\n",
    "3. only accounts for uncertainty due to too few samples, e.g. no uncertainty due to changes away from data\n",
    "4. does not account for boundary of area of interest \n",
    "5. assumes the samples are independent\n",
    "6. does not account for other local information sources\n",
    "\n",
    "The Bootstrap Approach (Efron, 1982)\n",
    "\n",
    "Statistical resampling procedure to calculate uncertainty in a calculated statistic from the data itself.\n",
    "* Does this work?  Prove it to yourself, for uncertainty in the mean solution is standard error: \n",
    "\n",
    "\\begin{equation}\n",
    "\\sigma^2_\\overline{x} = \\frac{\\sigma^2_s}{n}\n",
    "\\end{equation}\n",
    "\n",
    "Extremely powerful - could calculate uncertainty in any statistic!  e.g. P13, skew etc.\n",
    "* Would not be possible access general uncertainty in any statistic without bootstrap.\n",
    "* Advanced forms account for spatial information and sampling strategy (game theory and Journel’s spatial bootstrap (1993).\n",
    "\n",
    "Steps: \n",
    "\n",
    "1. assemble a sample set, must be representative, reasonable to assume independence between samples\n",
    "\n",
    "2. optional: build a cumulative distribution function (CDF)\n",
    "    * may account for declustering weights, tail extrapolation\n",
    "    * could use analogous data to support\n",
    "\n",
    "3. For $\\ell = 1, \\ldots, L$ realizations, do the following:\n",
    "\n",
    "    * For $i = \\alpha, \\ldots, n$ data, do the following:\n",
    "\n",
    "        * Draw a random sample with replacement from the sample set or Monte Carlo simulate from the CDF (if available). \n",
    "\n",
    "6. Calculate a realization of the sammary statistic of interest from the $n$ samples, e.g. $m^\\ell$, $\\sigma^2_{\\ell}$. Return to 3 for another realization.\n",
    "\n",
    "7. Compile and summarize the $L$ realizations of the statistic of interest.\n",
    "\n",
    "This is a very powerful method.  Let's try it out.\n",
    "\n",
    "\n",
    "#### Objective \n",
    "\n",
    "Provide an example and demonstration for:\n",
    "\n",
    "1. interactive plotting in Jupyter Notebooks with Python packages matplotlib and ipywidgets\n",
    "2. provide an intuitive hands-on example of statistical boostrap   \n",
    "\n",
    "#### Getting Started\n",
    "\n",
    "Here's the steps to get setup in Python with the GeostatsPy package:\n",
    "\n",
    "1. Install Anaconda 3 on your machine (https://www.anaconda.com/download/). \n",
    "2. Open Jupyter and in the top block get started by copy and pasting the code block below from this Jupyter Notebook to start using the geostatspy functionality. \n",
    "\n",
    "#### Load the Required Libraries\n",
    "\n",
    "The following code loads the required libraries."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "from ipywidgets import interactive                        # widgets and interactivity\n",
    "from ipywidgets import widgets                            \n",
    "from ipywidgets import Layout\n",
    "from ipywidgets import Label\n",
    "from ipywidgets import VBox, HBox\n",
    "import matplotlib.pyplot as plt                           # plotting\n",
    "import numpy as np                                        # working with arrays\n",
    "import pandas as pd                                       # working with DataFrames\n",
    "from scipy.stats import triang                            # parametric distributions\n",
    "from scipy.stats import binom\n",
    "from scipy.stats import norm\n",
    "from scipy.stats import uniform\n",
    "from scipy.stats import triang\n",
    "from scipy import stats                                   # statistical calculations\n",
    "import random                                             # random drawing / bootstrap realizations of the data"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Make a Synthetic Dataset\n",
    "\n",
    "This is an interactive method to:\n",
    "\n",
    "* select a parametric distribution\n",
    "\n",
    "* select the distribution parameters\n",
    "\n",
    "* select the number of samples and visualize the synthetic dataset distribution"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# parameters for the synthetic dataset\n",
    "bins = np.linspace(0,1000,1000)\n",
    "\n",
    "# interactive calculation of the sample set (control of source parametric distribution and number of samples)\n",
    "l = widgets.Text(value='                                      Boostrap Demonstration, Michael Pyrcz, Associate Professor, The University of Texas at Austin',layout=Layout(width='950px', height='30px'))\n",
    "dist = widgets.Dropdown(\n",
    "    options=['Triangular', 'Uniform', 'Gaussian'],\n",
    "    value='Gaussian',\n",
    "    description='Dataset Distribution:',\n",
    "    disabled=False,\n",
    "    layout=Layout(width='200px', height='30px')\n",
    ")\n",
    "a = widgets.FloatSlider(min=0.0, max = 100.0, value = 50.0, description = 'Dataset: Mean / Mode',orientation='vertical',layout=Layout(width='170px', height='200px'),continuous_update=False)\n",
    "a.style.handle_color = 'blue'\n",
    "d = widgets.FloatSlider(min=0.01, max = 30.0, value = 10.0, step = 1.0, description = 'Dataset: St. Deviation',orientation='vertical',layout=Layout(width='130px', height='200px'),continuous_update=False)\n",
    "d.style.handle_color = 'green'\n",
    "b = widgets.FloatSlider(min = 0, max = 100.0, value = 0.0, description = 'Dataset: Minimum',orientation='vertical',layout=Layout(width='130px', height='200px'),continuous_update=False)\n",
    "b.style.handle_color = 'red'\n",
    "c = widgets.IntSlider(min = 0, max = 100, value = 100, description = 'Dataset: Maximum',orientation='vertical',layout=Layout(width='130px', height='200px'),continuous_update=False)\n",
    "c.style.handle_color = 'orange'\n",
    "n = widgets.IntSlider(min = 2, max = 1000, value = 100, description = 'Dataset: Number Samples',orientation='vertical',layout=Layout(width='180px', height='200px'),continuous_update=False)\n",
    "n.style.handle_color = 'gray'\n",
    "\n",
    "ui = widgets.HBox([dist,a,d,b,c,n],)                      # basic widget formatting           \n",
    "ui2 = widgets.VBox([l,ui],)\n",
    "\n",
    "def f_make(dist,a, b, c, d, n):                           # function to take parameters, make sample and plot\n",
    "    global df\n",
    "    dataset = make_data(dist,a, b, c, d, n)\n",
    "    df = pd.DataFrame({'DataSet':dataset})\n",
    "    plt.subplot(111) \n",
    "    plt.hist(\n",
    "        dataset,\n",
    "        alpha=0.8,\n",
    "        color=\"darkorange\",\n",
    "        edgecolor=\"black\",\n",
    "        bins=bins) \n",
    "    plt.xlim(0.0,100.0); plt.title('Synthetic Dataset'); plt.ylabel('Frequency'); plt.xlabel('Data Values')\n",
    "            \n",
    "    plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.6, wspace=0.2, hspace=0.3)\n",
    "    plt.show()\n",
    "    return df\n",
    "\n",
    "def make_data(dist,a, b, c, d, n):                        # function to check parameters and make sample   \n",
    "    if dist == 'Uniform':\n",
    "        if b >= c:\n",
    "            print('Invalid uniform distribution parameters')\n",
    "            return None\n",
    "        dataset = uniform.rvs(size=n, loc = b, scale = c, random_state = 73073).tolist()\n",
    "        return dataset\n",
    "    elif dist == 'Triangular':\n",
    "        interval = c - b\n",
    "        if b >= a or a >= c or interval <= 0:\n",
    "            print('Invalid triangular distribution parameters')\n",
    "            return None        \n",
    "        dataset = triang.rvs(size=n, loc = b, c = (a-b)/interval, scale = interval, random_state = 73073).tolist()\n",
    "        return dataset\n",
    "    elif dist == 'Gaussian':\n",
    "        dataset = norm.rvs(size=n, loc = a, scale = d, random_state = 73073).tolist()\n",
    "        return dataset\n",
    "\n",
    "# connect the function to make the samples and plot to the widgets    \n",
    "interactive_plot = widgets.interactive_output(f_make, {'dist': dist,'a': a, 'd': d, 'b': b, 'c': c, 'n': n})\n",
    "interactive_plot.clear_output(wait = True)                # reduce flickering by delaying plot updating"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Display the GUI for Building the Synthetic Dataset\n",
    "\n",
    "We display the GUI now.  Select the desired parametric distribution and associated parameters.\n",
    "\n",
    "* if the parameters are invalid (e.g. traingular mode > max) an error message should display."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "dffaa1ae75074199b7219cf5e1868252",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(Text(value='                                      Boostrap Demonstration, Michael Pyrcz, Associ…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "5aa317dca5ed47828b75b10e7a6c6e7e",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Output()"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(ui2, interactive_plot)                            # display the interactive plot"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We now have a synthetic dataset to work with. Now we can:\n",
    "\n",
    "* assign the samples to a 1D ndarray \n",
    "\n",
    "* make a DataFrame with the samples\n",
    "\n",
    "* check the summary statistics\n",
    "\n",
    "This is our sample set that we will apply ot bootstrap.\n",
    "\n",
    "#### Perform Bootstrap \n",
    "\n",
    "Now we take our synthetic dataset, sampled from the parametric distributioin above, and apply it to statistical bootstrap.\n",
    "\n",
    "* we calculate the sampling distributions / uncertainty in the mean and standard deviation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 17552 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Summary Statistics for Bootstrap for Uncertainty in the Mean:\n",
      "DescribeResult(nobs=1000, minmax=(46.4911862844161, 53.39117667902819), mean=50.268604394491426, variance=0.8519541651807531, skewness=0.08312739639977067, kurtosis=0.1175301689146524)\n",
      "P10: 49.064, P50: 50.261, P90: 51.421\n",
      "\n",
      "Summary Statistics for Bootstrap for Uncertainty in the Standard Deviation:\n",
      "DescribeResult(nobs=1000, minmax=(7.158715123778451, 11.71402702898678), mean=9.276719117116285, variance=0.4816916908140478, skewness=0.23332808535545702, kurtosis=0.08924833308776536)\n",
      "P10: 8.434, P50: 9.241, P90: 10.152\n"
     ]
    }
   ],
   "source": [
    "L = 1000                                                  # set the number of realizations\n",
    "mean = np.zeros(L); stdev = np.zeros(L)                   # declare arrays to hold the realizations of the statistics\n",
    "for l in range(0, L):                                     # loop over realizations\n",
    "    samples = random.choices(df['DataSet'].values, weights=None, cum_weights=None, k=len(df))\n",
    "    mean[l] = np.average(samples)\n",
    "    stdev[l] = np.std(samples)\n",
    "    \n",
    "plt.subplot(121)                                          # plot the distribution for uncertainty in the mean     \n",
    "plt.hist(mean,alpha=0.8,color=\"darkorange\",edgecolor=\"black\",bins=np.linspace(0,100,40)); plt.xlim(0,100); plt.ylabel('Frequency'); plt.xlabel('Values'); plt.title('Bootstrap Uncertainty in the Mean')\n",
    "\n",
    "plt.subplot(122)                                          # plot the distribution for uncertainty in the standard deviation\n",
    "plt.hist(stdev,alpha=0.8,color=\"darkorange\",edgecolor=\"black\",bins=np.linspace(0,40,40)); plt.xlim(0,40); plt.ylabel('Frequency'); plt.xlabel('Values'); plt.title('Bootstrap Uncertainty in the Standard Deviation')\n",
    "\n",
    "plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.2, wspace=0.2, hspace=0.2)\n",
    "plt.show()   \n",
    "\n",
    "# provide summary statistics, P10, P50 and P90\n",
    "print('Summary Statistics for Bootstrap for Uncertainty in the Mean:') \n",
    "print(stats.describe(mean))\n",
    "print('P10: ' + str(round(np.percentile(mean,10),3)) + ', P50: ' + str(round(np.percentile(mean,50),3)) + ', P90: ' + str(round(np.percentile(mean,90),3))) \n",
    "\n",
    "print('\\nSummary Statistics for Bootstrap for Uncertainty in the Standard Deviation:')\n",
    "print(stats.describe(stdev))\n",
    "print('P10: ' + str(round(np.percentile(stdev,10),3)) + ', P50: ' + str(round(np.percentile(stdev,50),3)) + ', P90: ' + str(round(np.percentile(stdev,90),3)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Change the Number of Data\n",
    "\n",
    "Let's change the number of data drawn to observe the change in uncertainty\n",
    "\n",
    "* we will assume the same dataset and not recalculate it each time\n",
    "\n",
    "* we will just sample with replacement with the new number of samples for each bootstrap realization\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "# parameters for the synthetic dataset\n",
    "bins = np.linspace(0,1000,1000)\n",
    "\n",
    "l = widgets.Text(value='                           Boostrap Demonstration with Modified Number of Data, Michael Pyrcz, Associate Professor, The University of Texas at Austin',layout=Layout(width='950px', height='30px'))\n",
    "\n",
    "n = widgets.IntSlider(min = 2, max = 1000, value = 100, description = 'New Number Samples',orientation='horizontal',layout=Layout(width='800px', height='20px'),continuous_update=False)\n",
    "n.style.handle_color = 'gray'\n",
    "\n",
    "ui3 = widgets.VBox([l,n],)\n",
    "\n",
    "def f_rerun(n):\n",
    "    L = 1000                                 # set the number of realizations\n",
    "    mean2 = np.zeros(L); stdev2 = np.zeros(L)    # declare arrays to hold the realizations of the statistics\n",
    "    for l in range(0, L):                      # loop over realizations\n",
    "        samples = random.choices(df['DataSet'].values, weights=None, cum_weights=None, k=n)\n",
    "        mean2[l] = np.average(samples)\n",
    "        stdev2[l] = np.std(samples)\n",
    "    \n",
    "    plt.subplot(121)\n",
    "    plt.hist(mean,alpha=0.5,color=\"red\",edgecolor=\"black\",bins=np.linspace(0,100,40),label='Original'); plt.xlim(0,100); plt.ylabel('Frequency'); plt.xlabel('Values'); plt.title('Bootstrap Uncertainty in the Mean')\n",
    "    plt.hist(mean2,alpha=0.5,color=\"blue\",edgecolor=\"black\",bins=np.linspace(0,100,40),label='New'); plt.xlim(0,100); plt.ylabel('Frequency'); plt.xlabel('Values'); plt.title('Bootstrap Uncertainty in the Mean')\n",
    "    plt.legend()\n",
    "\n",
    "    plt.subplot(122)\n",
    "    plt.hist(stdev,alpha=0.5,color=\"red\",edgecolor=\"black\",bins=np.linspace(0,40,40),label='Original'); plt.xlim(0,40); plt.ylabel('Frequency'); plt.xlabel('Values'); plt.title('Bootstrap Uncertainty in the Standard Deviation')\n",
    "    plt.hist(stdev2,alpha=0.5,color=\"blue\",edgecolor=\"black\",bins=np.linspace(0,40,40),label='New'); plt.xlim(0,40); plt.ylabel('Frequency'); plt.xlabel('Values'); plt.title('Bootstrap Uncertainty in the Standard Deviation')\n",
    "    plt.legend()\n",
    "    \n",
    "    plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.2, wspace=0.2, hspace=0.2)  \n",
    "    plt.show()  \n",
    "\n",
    "interactive_plot3 = widgets.interactive_output(f_rerun, {'n': n})\n",
    "interactive_plot3.clear_output(wait = True)                # reduce flickering by delaying plot updating"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Display the GUI for Modifying the Number of Data\n",
    "\n",
    "We display the GUI now. Select the desired number of data\n",
    "\n",
    "* observed the change (new) vs. the original (original) bootstrap uncertainty as the number of data ranges from less than to greater than the original number of data  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "43dec4b70b99457bac577788df647617",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(Text(value='                           Boostrap Demonstration with Modified Number of Data, Mic…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "6dfa183b6c384f8db5617708d2ec39ea",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Output(outputs=({'output_type': 'display_data', 'data': {'text/plain': '<Figure size 432x288 with 2 Axes>', 'i…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(ui3, interactive_plot3)                            # display the interactive plot"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Observations\n",
    "\n",
    "Some observations form changing the number of data:\n",
    "\n",
    "* with less data the uncertainty increases\n",
    "\n",
    "* with more data the uncertianty decreases \n",
    "\n",
    "* there is a bias low in standard deviation with too few samples, as we fail to observe the dispersion well\n",
    "\n",
    "\n",
    "#### Comments\n",
    "\n",
    "This was a simple demonstration of interactive plots in Jupyter Notebook Python with the ipywidgets and matplotlib packages. \n",
    "\n",
    "I have many other demonstrations on data analytics and machine learning, e.g. on the basics of working with DataFrames, ndarrays, univariate statistics, plotting data, declustering, data transformations, trend modeling and many other workflows available at https://github.com/GeostatsGuy/PythonNumericalDemos and https://github.com/GeostatsGuy/GeostatsPy. \n",
    "  \n",
    "I hope this was helpful,\n",
    "\n",
    "*Michael*\n",
    "\n",
    "#### The Author:\n",
    "\n",
    "### Michael Pyrcz, Associate Professor, University of Texas at Austin \n",
    "*Novel Data Analytics, Geostatistics and Machine Learning Subsurface Solutions*\n",
    "\n",
    "With over 17 years of experience in subsurface consulting, research and development, Michael has returned to academia driven by his passion for teaching and enthusiasm for enhancing engineers' and geoscientists' impact in subsurface resource development. \n",
    "\n",
    "For more about Michael check out these links:\n",
    "\n",
    "#### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)\n",
    "\n",
    "#### Want to Work Together?\n",
    "\n",
    "I hope this content is helpful to those that want to learn more about subsurface modeling, data analytics and machine learning. Students and working professionals are welcome to participate.\n",
    "\n",
    "* Want to invite me to visit your company for training, mentoring, project review, workflow design and / or consulting? I'd be happy to drop by and work with you! \n",
    "\n",
    "* Interested in partnering, supporting my graduate student research or my Subsurface Data Analytics and Machine Learning consortium (co-PIs including Profs. Foster, Torres-Verdin and van Oort)? My research combines data analytics, stochastic modeling and machine learning theory with practice to develop novel methods and workflows to add value. We are solving challenging subsurface problems!\n",
    "\n",
    "* I can be reached at mpyrcz@austin.utexas.edu.\n",
    "\n",
    "I'm always happy to discuss,\n",
    "\n",
    "*Michael*\n",
    "\n",
    "Michael Pyrcz, Ph.D., P.Eng. Associate Professor The Hildebrand Department of Petroleum and Geosystems Engineering, Bureau of Economic Geology, The Jackson School of Geosciences, The University of Texas at Austin\n",
    "\n",
    "#### More Resources Available at: [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "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.9.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
