{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "ede095b1",
   "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 Model Fitting\n",
    "\n",
    "### Michael J. Pyrcz, Professor, The University of Texas at Austin \n",
    "\n",
    "*Novel Data Analytics, Geostatistics and Machine Learning Subsurface Solutions*"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2e15b023",
   "metadata": {},
   "source": [
    "#### Fitting a Model\n",
    "\n",
    "There are two common methods to fit a model, ordinary least squares and maximim likelihood estimation. Here I provide a short description and then demonstrate them together for fitting a parametric Gaussian distribution to a synthetic data set.\n",
    "\n",
    "First, let's define the model and data.\n",
    "\n",
    "* The model parameters, $\\beta$, are trained to the training data, $X_{\\alpha}, \\alpha = 1,\\ldots,n$.\n",
    "* In this example the model is a parametric Gaussian distribution; therefore the model parameters are the mean, $\\mu$, and standard deviation, $\\sigma$.\n",
    "* For this example, the synthetic data are a limited set of independent samples from an 'unknown' Gaussian distributions; therefore, we expect that our model selection should be reasonable, i.e., the data is somewhat Gaussian distributed.\n",
    "\n",
    "##### Ordinary Least Squares Fitting\n",
    "\n",
    "This is a very common method for fitting a model, let's minimize the error between the model predictions and the observations. \n",
    "\n",
    "* We calculated the model predictions, $\\hat{y}_{\\alpha}$, at training data locations, $\\alpha = 1,\\ldots,n$.  \n",
    "* Then we compare the model predictions to the true data observations as the squared error, $\\Delta y_{\\alpha}^2 = \\left(\\hat{y}_{\\alpha} - y_{\\alpha} \\right)^2$, where the model predictions, $\\hat{y}_{\\alpha} = \\hat{f}_{\\beta}(X_{\\alpha})$, are estimated with our estimated model parameters, $\\hat{f}_{\\beta}(X_{\\alpha})$.\n",
    "* We then sum the squared error over all data observations, the sum of squared error (SSE) is $\\sum_{\\alpha=1}^n \\left(\\hat{y}_{\\alpha} - y_{\\alpha} \\right)^2$\n",
    "\n",
    "Now we pose the model parameter estimation problem as an optimization problem to select the model parameters such that we minimize the SSE.\n",
    "\n",
    "\\begin{equation}\n",
    "\\hat{\\beta}^{OLS}= {\\text{arg min}}_{\\beta} \\rightarrow \\left(\\hat{f}_{\\beta}(X_{\\alpha}) - y_{\\alpha} \\right)^2\n",
    "\\end{equation}\n",
    "\n",
    "##### Maximum Likelihood Fitting\n",
    "\n",
    "This is a different way to approach fitting a model, find the model parameters, $\\beta$, that maximizes the probability of the sample data, $X$. \n",
    "\n",
    "For the Bayesian approach to machine learning, this is the **likelihood** that is applied to the **prior** to calculate the **posterior**. Under the assumption of data independence we apply the product sum to the probability of each of the data, $X_\\alpha = 1,\\ldots,n$, given the model:\n",
    "\n",
    "\\begin{equation}\n",
    "P(X | \\hat{f}_{\\beta}) = \\prod_{\\alpha = 1}^{n} P(X_{\\alpha}|\\hat{f}_{\\beta}(X)), \\alpha = 1,\\ldots,n\n",
    "\\end{equation}\n",
    "\n",
    "For our maximization it is sufficient to work with likelihoods, the density values directly from the PDF model, $\\hat{f}_{\\beta}(X_{\\alpha})$.\n",
    "\n",
    "\\begin{equation}\n",
    "P(X | \\hat{f}_{\\beta}) = \\prod_{\\alpha = 1}^{n} \\hat{f}_{\\beta}(X_{\\alpha}), \\alpha = 1,\\ldots,n\n",
    "\\end{equation}\n",
    "\n",
    "This solution can be very small given a large number of sample data, so we commonly apply the log transform to work with the sum of the log likelihoods and we calculate the negative sum of log likelihoods to convert this to a standard minimization problem.\n",
    "\n",
    "\\begin{equation}\n",
    "P(X | \\hat{f}_{\\beta}(X)) = -\\sum_{\\alpha = 1}^{n} log \\left(\\hat{f}_{\\beta}(X_{\\alpha})\\right), \\alpha = 1,\\ldots,n\n",
    "\\end{equation}\n",
    "\n",
    "Now we pose the model parameter estimation problem as an optimization problem to select the model parameters such that we minimize the negative log likelihood.\n",
    "\n",
    "\\begin{equation}\n",
    "\\hat{\\beta}^{OLS}= {\\text{arg min}}_{\\beta} \\rightarrow P(X | \\hat{f}_{\\beta}(X))\n",
    "\\end{equation}\n",
    "\n",
    "#### Comparing OLS and MLE Approaches\n",
    "\n",
    "We calculate the model parameters, such that we:\n",
    "\n",
    "**OLS**: - minimize the sum of the squared differences between the observed values and the values predicted by the model.\n",
    "\n",
    "**MLE**: - maximize the likelihood of the observed data given the model.\n",
    "\n",
    "Both methods assume sufficient sample data, sample data representativity, correct model specification, in adddition:\n",
    "\n",
    "**OLS**: - assumes that the errors have a mean of zero, constant variance and are independent of eachother (no correlation in error).\n",
    "\n",
    "Wit\n",
    "\n",
    "\n",
    "#### Load and Configure the Required Libraries\n",
    "\n",
    "The following code loads the required libraries and sets a plotting default."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "da837ef7",
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "seed=73073                                              \n",
    "supress_warnings = False\n",
    "import os                                               # to set current working directory \n",
    "import sys                                              # supress output to screen for interactive variogram modeling\n",
    "import numpy as np                                      # arrays and matrix math\n",
    "import pandas as pd                                     # DataFrames\n",
    "from scipy.optimize import curve_fit\n",
    "from scipy.optimize import minimize\n",
    "from scipy.stats import norm                            # Gaussian PDF\n",
    "import matplotlib.pyplot as plt                         # plotting\n",
    "import seaborn as sns                                   # plot PDF\n",
    "from sklearn.model_selection import train_test_split    # train and test split\n",
    "from sklearn import tree                                # tree program from scikit learn (package for machine learning)\n",
    "from sklearn import metrics                             # measures to check our models\n",
    "import scipy.stats as stats                             #search for neighbours\n",
    "from matplotlib.patches import Rectangle                # build a custom legend\n",
    "from matplotlib.ticker import (MultipleLocator, AutoMinorLocator) # control of axes ticks\n",
    "import math                                             # sqrt operator\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",
    "cmap = plt.cm.inferno                                   # default color bar, no bias and friendly for color vision defeciency\n",
    "plt.rc('axes', axisbelow=True)                          # grid behind plotting elements\n",
    "if supress_warnings == True:\n",
    "    import warnings                                     # supress any warnings for this demonstration\n",
    "    warnings.filterwarnings('ignore')                  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2b57659e",
   "metadata": {},
   "source": [
    "#### Declare Functions\n",
    "\n",
    "The following functions for clean code. \n",
    "\n",
    "* Just a improved grid for the plot.\n",
    "\n",
    "* Gaussian negative log likelihood function modified from [StackExchange](https://stats.stackexchange.com/questions/504004/how-do-we-code-a-maximum-likelihood-fitting-for-a-simple-gaussian-data) solution from jkpate."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "a333fd85",
   "metadata": {},
   "outputs": [],
   "source": [
    "def add_grid():\n",
    "    plt.gca().grid(True, which='major',linewidth = 1.0); plt.gca().grid(True, which='minor',linewidth = 0.2) # add y grids\n",
    "    plt.gca().tick_params(which='major',length=7); plt.gca().tick_params(which='minor', length=4)\n",
    "    plt.gca().xaxis.set_minor_locator(AutoMinorLocator()); plt.gca().yaxis.set_minor_locator(AutoMinorLocator()) # turn on minor ticks\n",
    "    \n",
    "def gaussian_negloglike(params):    # Calculate sum negative log likelihood\n",
    "    mu = params[0]; sigma = params[1]\n",
    "    neg_log_likelihood = -1*np.sum(stats.norm.logpdf(X, loc=mu, scale=sigma)) \n",
    "    return neg_log_likelihood"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ec5bb17b",
   "metadata": {},
   "source": [
    "#### Make and Display a Synthetic Dataset\n",
    "\n",
    "We draw random samples from a a parametric Gaussian distritubion and visualize the data-derived nonparametric and the Gaussian parametric CDF, fit with: \n",
    "\n",
    "* OLS - ordinary least squares\n",
    "\n",
    "* MLE - maximum likelihood estimation "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "a59e3cb8",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 65772 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "n = 35; tmean = 0.5; tstdev = 0.1; xmin=0.0; xmax=1.0\n",
    "np.random.seed(seed=seed)                               # set random number seed\n",
    "Xval = np.linspace(xmin,xmax,100)\n",
    "X = np.random.normal(loc=tmean,scale=tstdev,size=n-2); X = np.append(X,[0.8,0.9])\n",
    "X = np.sort(X); CDF = np.arange(1,n+1,1)/(n+1)\n",
    "\n",
    "mean = np.average(X); stdev = np.std(X)\n",
    "\n",
    "mean_ls,stdev_ls = curve_fit(norm.cdf,X,CDF,p0=[0,1],method='lm')[0] \n",
    "\n",
    "mean_ml,stdev_ml = minimize(gaussian_negloglike,x0=[0,1],method='Nelder-Mead').x\n",
    "\n",
    "plt.subplot(121)\n",
    "plt.scatter(np.sort(X),np.arange(1,n+1,1)/(n+1),c='black',s=10,edgecolor='black',zorder=10)\n",
    "plt.title('Copper Distribution via Parameter Estimation')\n",
    "# plt.plot(Xval,stats.norm.cdf(Xval,loc=mean,scale=stdev),c='black',zorder=1,label='Parameter Inference')\n",
    "plt.plot(Xval,stats.norm.cdf(Xval,loc=mean_ls,scale=stdev_ls),c='red',zorder=1,label='OLS')\n",
    "plt.plot(Xval,stats.norm.cdf(Xval,loc=mean_ml,scale=stdev_ml),c='blue',zorder=1,label='MLE')\n",
    "\n",
    "plt.legend(loc='upper left')\n",
    "plt.ylim([0,1]); plt.xlim([0,1.0]); add_grid(); plt.xlabel('CU Grade (g/t)'); plt.ylabel('Cumulative Probability')\n",
    "\n",
    "plt.subplots_adjust(left=0.0,bottom=0.0,right=3.0,top=1.2); plt.show() # set plot size "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8d191a1a",
   "metadata": {},
   "source": [
    "#### Make the Interactive Dashboard\n",
    "\n",
    "Change the mean and standard deviation of a Gaussian model and observe the ordinary least squares and maximum likelihood assessment of fit."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "f7b6a918",
   "metadata": {},
   "outputs": [],
   "source": [
    "l = widgets.Text(value='                                  Interactive Statistical Model Fitting Demo, Prof. Michael Pyrcz, The University of Texas at Austin',\n",
    "                 layout=Layout(width='750px', height='30px'))\n",
    "\n",
    "mean = widgets.FloatSlider(min=0.0, max = 1.0, value=0.5, step = 0.02, description = '$\\mu$',orientation='horizontal', \n",
    "           style = {'description_width': 'initial'},layout=Layout(width='370px', height='30px'),continuous_update=False)\n",
    "stdev = widgets.FloatSlider(min=0.01, max = 1.0, value=0.1, step = 0.02, description = r'$\\sigma$',orientation='horizontal',\n",
    "           style = {'description_width': 'initial'},layout=Layout(width='370px', height='30px'),continuous_update=False)\n",
    "\n",
    "ui = widgets.HBox([mean,stdev],)\n",
    "ui2 = widgets.VBox([l,ui],)\n",
    "\n",
    "def run_plot(mean,stdev):\n",
    "    np.random.seed(seed=seed)                               # set random number seed\n",
    "    Xval = np.linspace(xmin,xmax,100)\n",
    "    \n",
    "    CDF_hat = norm.cdf(X,loc = mean,scale=stdev)            # for specified mean and stdev calculate the PDF and CDF\n",
    "    pdf_hat = stats.norm.pdf(X,loc=mean,scale=stdev)\n",
    "    sq_err = (CDF-CDF_hat)**2                               # calculate the square error of the CDF\n",
    "    sse = np.sum(sq_err)\n",
    "    prod_like = np.product(pdf_hat)                         # calculate the likelihoof from the PDF\n",
    "    neg_log_like = -np.sum(np.log(pdf_hat))\n",
    "    \n",
    "    mean_OLS,stdev_OLS = curve_fit(norm.cdf,np.sort(X),np.arange(1,n+1,1)/(n+1), p0=[0,1],method='lm')[0] # calculate OLS and error\n",
    "    sq_err_OLS = (CDF-norm.cdf(X,loc = mean_OLS,scale=stdev_OLS))**2\n",
    "    sse_OLS = np.sum(sq_err_OLS)\n",
    "    \n",
    "    mean_MLE,stdev_MLE = minimize(gaussian_negloglike,x0=[0,1],method='Nelder-Mead').x # calculate MLE and likelihood\n",
    "    pdf_hat_MLE = stats.norm.pdf(X,loc=mean_MLE,scale=stdev_MLE)\n",
    "    prod_like_MLE = np.product(pdf_hat_MLE) \n",
    "    neg_log_like_MLE = -np.sum(np.log(pdf_hat_MLE))\n",
    "    \n",
    "    plt.subplot(221)                                         # plot CDF and errors and OLS solution\n",
    "    plt.scatter(np.sort(X),np.arange(1,n+1,1)/(n+1),c='black',s=10,edgecolor='black',zorder=10)\n",
    "    plt.title('Ordinary Least Squares - Model and Data Error')\n",
    "    plt.plot(Xval,stats.norm.cdf(Xval,loc=mean,scale=stdev),c='red',lw=3,label=r'$\\hat{F}_{Cu}(\\alpha)$',zorder=100)\n",
    "    plt.plot(Xval,stats.norm.cdf(Xval,loc=mean_OLS,scale=stdev_OLS),c='grey',alpha=0.3,lw=3,label=r'$\\hat{F}_{Cu}^{OLS}(\\alpha)$',zorder=10)\n",
    "    for i in range(0,len(X)):\n",
    "        plt.plot([X[i],X[i]],[CDF[i],CDF_hat[i]],color='black',alpha=0.3,zorder=1)\n",
    "    plt.legend(loc='upper left')\n",
    "    plt.ylim([0,1]); plt.xlim([0,1.0]); add_grid(); plt.xlabel('Copper Grade (g/t)'); plt.ylabel(r'Cumulative Probability, $F_{Cu}$')\n",
    "    plt.annotate(r'Current Model: $\\mu = $' + str(np.round(mean,2)),xy=[0.68,0.4],c='red')\n",
    "    plt.annotate(r'$\\sigma = $' + str(np.round(stdev,2)),xy=[0.80,0.35],c='red')\n",
    "    plt.annotate(r'OLS Solution: $\\mu = $' + str(np.round(mean_OLS,2)),xy=[0.69,0.3],c='black')\n",
    "    plt.annotate(r'$\\sigma = $' + str(np.round(stdev_OLS,2)),xy=[0.80,0.25],c='black')\n",
    "    \n",
    "    plt.subplot(222)                                         # plot error distribution and OLS solution\n",
    "    plt.hist(sq_err,color='red',alpha=0.6,edgecolor='darkred',lw=2,bins=np.linspace(0.0,0.1,41),zorder=10)\n",
    "    plt.hist(sq_err_OLS,color='grey',alpha=1.0,edgecolor='black',lw=2,bins=np.linspace(0.0,0.1,41),zorder=1)\n",
    "    plt.xlabel(r'Squared Error, $\\left( \\hat{F}_{Cu}(\\alpha) - F_{Cu}(\\alpha) \\right)^2$'); plt.ylabel('Frequency'); plt.title('Ordinary Least Squares - Data Error Distribution')\n",
    "    add_grid(); plt.xlim([0.0,0.1]); plt.ylim([0,30])\n",
    "    plt.annotate(r'Minimize: $\\sum_{\\alpha=1}^n \\left( \\hat{F}_{Cu}(\\alpha) - F_{Cu}(\\alpha) \\right)^2 = $' + str(np.round(sse,2)),xy=[0.065,25.0],c='red')\n",
    "    plt.annotate(r'OLS: $\\sum_{\\alpha=1}^n \\left( \\hat{F}_{Cu}^{OLS}(\\alpha) - F_{Cu}(\\alpha) \\right)^2 = $' + str(np.round(sse_OLS,2)),xy=[0.069,22.0],c='black')\n",
    "    \n",
    "    plt.subplot(223)                                         # plot data likelihood, PDF and MLE solution\n",
    "    plt.scatter(np.sort(X),np.full(len(X),0.03),c='black',s=10,edgecolor='black',zorder=10)\n",
    "    plt.title('Copper Distribution via Parameter Estimation')\n",
    "    plt.plot(Xval,stats.norm.pdf(Xval,loc=mean,scale=stdev),c='blue',lw=3,label=r'$\\hat{f}_{Cu}(\\alpha)$',zorder=100)\n",
    "    plt.plot(Xval,stats.norm.pdf(Xval,loc=mean_MLE,scale=stdev_MLE),c='grey',alpha=0.3,lw=3,label=r'$\\hat{f}_{Cu}^{MLE}(\\alpha)$',zorder=10)\n",
    "    for i in range(0,len(X)):\n",
    "        plt.plot([X[i],X[i]],[0.0,pdf_hat[i]],color='black',alpha=0.3,zorder=1)\n",
    "    plt.annotate(r'Current Model: $\\mu = $' + str(np.round(mean,2)),xy=[0.68,3.5],c='blue')\n",
    "    plt.annotate(r'$\\sigma = $' + str(np.round(stdev,2)),xy=[0.80,3.3],c='blue')\n",
    "    plt.annotate(r'MLE Solution: $\\mu = $' + str(np.round(mean_MLE,2)),xy=[0.69,3.1],c='black')\n",
    "    plt.annotate(r'$\\sigma = $' + str(np.round(stdev_MLE,2)),xy=[0.80,2.9],c='black')\n",
    "    plt.xlabel('Copper Grade (g/t)'); plt.ylabel(r'Likelihood, Density, $f_{Cu}$'); plt.title('Maximum Likelihood - Model and Data Likelihood')\n",
    "    add_grid(); plt.xlim([0.0,1.0]); plt.ylim([0.0,4.0]); plt.legend(loc='upper left')\n",
    "    \n",
    "    plt.subplot(224)                                          # plot data likelihood distribution and MLS solution\n",
    "    plt.hist(pdf_hat,color='blue',alpha=0.5,edgecolor='darkblue',lw=2,bins=np.linspace(0.0,4.0,41),orientation='horizontal',zorder=100)\n",
    "    plt.hist(pdf_hat_MLE,color='grey',alpha=1.0,edgecolor='black',lw=2,bins=np.linspace(0.0,4.0,41),orientation='horizontal',zorder=10)\n",
    "    plt.xlabel('Frequency'); plt.ylabel(r'Likelihood, Density, $f_{Cu}$'); plt.title('Maximum Likelihood - Data Likelihood Distribution')\n",
    "    add_grid(); plt.xlim([0.0,10.0]); plt.ylim([0.0,4.0])\n",
    "    plt.annotate(r'Maximize: $\\prod_{\\alpha=1}^n \\hat{f}_\\alpha = $' + str(np.round(prod_like,2)),xy=[6.5,3.5],c='blue')\n",
    "    plt.annotate(r'MLE: $\\prod_{\\alpha=1}^n \\hat{f}_{Cu}^{MLE}(\\alpha) = $' + str(np.round(prod_like_MLE,2)),xy=[6.9,3.1],c='black')\n",
    "    plt.annotate(r'Minimize: $-\\sum_{\\alpha=1}^n log\\left[ \\hat{f}_{Cu}(\\alpha) \\right]  = $' + str(np.round(neg_log_like,2)),xy=[6.5,2.7],c='blue')\n",
    "    plt.annotate(r'MLE: $-\\sum_{\\alpha=1}^n \\left( log(\\hat{f}_{Cu}^{MLE}(\\alpha) )   \\right)  = $' + str(np.round(neg_log_like_MLE,2)),xy=[6.9,2.3],c='black')\n",
    "     \n",
    "    plt.subplots_adjust(left=0.0,bottom=0.0,right=3.0,top=2.2); plt.show() # set plot size \n",
    "    \n",
    "# connect the function to make the samples and plot to the widgets    \n",
    "interactive_plot = widgets.interactive_output(run_plot, {'mean':mean,'stdev':stdev})\n",
    "interactive_plot.clear_output(wait = True)               # reduce flickering by delaying plot updating  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "358a032f",
   "metadata": {},
   "source": [
    "### Interactive Statistical Model Fitting Demonstation \n",
    "\n",
    "#### Michael Pyrcz, Professor, The University of Texas at Austin \n",
    "\n",
    "Change the mean and standard deviation of a Gaussian model and observe the ordinary least squares and maximum likelihood assessment of fit.\n",
    "\n",
    "### The Inputs\n",
    "\n",
    "* Gaussian Parametric Model: **$\\mu$** - mean, **$\\sigma$** - standard deviation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "483fc030",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "ff55d389cee24d1a991ed8b4ccefe3f3",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(Text(value='                                  Interactive Statistical Model Fitting Demo, Prof.…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "bc861e8c8a5e49c991b93a6f1b075434",
       "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",
   "id": "07eb83a5",
   "metadata": {},
   "source": [
    "#### Comments\n",
    "\n",
    "This was a basic demonstration of squared error and data likelihood for OLS and MLE model fitting respectively. I have many other demonstrations and even basics of working with DataFrames, ndarrays, univariate statistics, plotting data, declustering, data transformations and many other workflows available at https://github.com/GeostatsGuy/PythonNumericalDemos and https://github.com/GeostatsGuy/GeostatsPy. \n",
    "  \n",
    "#### The Author:\n",
    "\n",
    "### Michael J. Pyrcz, Professor, The 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. Professor, The Hildebrand Department of Petroleum and Geosystems Engineering, Bureau of Economic Geology, 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,
   "id": "f51344db",
   "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.11.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
