{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a6885e33",
   "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 Density-based Clustering with DBSCAN \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": "60d9f326",
   "metadata": {},
   "source": [
    "#### DBSCAN Clustering\n",
    "\n",
    "The DBSCAN (Density-Based Spatial Clustering of Applications with Noise) approach finds clusters of samples (high density) is well suited to working with data with an arbitrary (even non-convex) shape and with noise.\n",
    "\n",
    "For a complete lecture with linked Python workflows check out:\n",
    "\n",
    "* [Density-based Clustering Lecture](https://www.youtube.com/watch?v=3GaLe8HaDMc&list=PLG19vXLQHvSC2ZKFIkgVpI9fCjkN38kwf&index=15)\n",
    "\n",
    "Here's a complete Python workflow and more details DBSCAN:\n",
    "\n",
    "* [DBSCAN workflow](https://github.com/GeostatsGuy/PythonNumericalDemos/blob/master/SubsurfaceDataAnalytics_advanced_clustering.ipynb)\n",
    "\n",
    "The two DBSCAN hyperparameters include:\n",
    "\n",
    "* **eps** - maximum distance between to samples in features space for a sample to be included in the neighbourhood of the other sample. Too small a value will result in many clusters and outliers, and too large a value will result in one cluster with all the data together.\n",
    "\n",
    "* **min_samples** - minimum number of samples in a neighbourhood to spawn a new cluster. Use a larger value for noisy data to prevent identifying clusters due to a few outliers.\n",
    "\n",
    "The method proceeds through the dataset and assigns the samples as:\n",
    "\n",
    "* **core** point if there are $\\ge$ **min_samples** within **eps** distance from the sample\n",
    "\n",
    "* **border** point if there are $lt$ **min_samples** within **eps** distance from the sample, but the sample is within **eps** distance of a core point.\n",
    "\n",
    "* **outlier** point if there are $lt$ **min_samples** within **eps** distance from the sample and the sample is not within **eps** distance of a core point\n",
    "\n",
    "Once the points are assigned these labels, all connected core points and their associate border points are assigned to an unique cluster.  Outlier points are left as outliers without an assigned cluster.  To understand the cluster assigments we should explain the following forms of connection.\n",
    "\n",
    "**directly density reachable** - point X is directly density reachable from A, if A is a core point and X belongs to the neighborhood (distance $le$ eps) from A.\n",
    "\n",
    "**density-reachable** - point Y is density reachable from A if Y belongs to a neighborhood of a core point that can reached from A. This would require a chain of core points each belonging the previous core points and the last core point including point Y.\n",
    "\n",
    "**density-connected** - points A and B are density-connected if there is a point Z that is density-reachable from both points A and B.\n",
    "\n",
    "**density-based cluster** - a nonempty set where all points are density-connected to eachother. \n",
    "\n",
    "The approach iterates as follows:\n",
    "\n",
    "1. all points are labled as unvisited\n",
    "\n",
    "2. randomly visit an unvisited sample\n",
    "\n",
    "3. check if a core point ($ge$ min_sample within eps distance), if so label as core otherwise label as outlier\n",
    "\n",
    "4. now visit all points within eps distance of the core point, determine if core, otherwise label as border point\n",
    "\n",
    "5. recusive operation where all points within eps distance of new core points are checked\n",
    "\n",
    "6. once this is exhausted then randomly visit an unvisited point\n",
    "\n",
    "This apporach may be thought of as identify and grow/merge clusters guided by local point density.\n",
    "\n",
    "\n",
    "After some careful interations of these parameters we get the following result.\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": "ecc3ed57",
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\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",
    "import matplotlib.pyplot as plt                         # plotting\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.spatial as spatial                         #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": "aaacbb36",
   "metadata": {},
   "source": [
    "#### Declare Functions\n",
    "\n",
    "The following functions for clean code. \n",
    "\n",
    "* Just a improved grid for the plot."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "64952c7c",
   "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"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d36bf017",
   "metadata": {},
   "source": [
    "#### Recursive Method to Perform DBSCAN \n",
    "\n",
    "I wrote this recursive method to perform DBSCAN. Then I used it in the dashboard below.\n",
    "\n",
    "* Recusursion can be tricky to code so I left my debugging outputs below to demonstrate how this was made and checked."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "088d9559",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "at point:5, step: 0\n",
      "[]\n",
      "step: 1\n",
      "at point:37, step: 1\n",
      "[43, 30, 42]\n",
      "at point:43, step: 2\n",
      "[37, 30, 14, 42, 62]\n",
      "at point:30, step: 3\n",
      "[37, 43, 76, 74]\n",
      "at point:76, step: 4\n",
      "[30, 74]\n",
      "step: 5\n",
      "at point:74, step: 5\n",
      "[30, 76, 6]\n",
      "at point:6, step: 6\n",
      "[94, 74]\n",
      "step: 7\n",
      "step: 7\n",
      "step: 7\n",
      "at point:14, step: 7\n",
      "[67, 43, 42, 62]\n",
      "at point:67, step: 8\n",
      "[17, 89, 14]\n",
      "at point:17, step: 9\n",
      "[79, 97, 67]\n",
      "at point:79, step: 10\n",
      "[66, 17]\n",
      "step: 11\n",
      "at point:97, step: 11\n",
      "[17, 38, 15, 89, 41]\n",
      "at point:38, step: 12\n",
      "[91, 97, 15, 41]\n",
      "at point:91, step: 13\n",
      "[29, 13, 20, 23, 38]\n",
      "at point:29, step: 14\n",
      "[77, 13, 20, 91]\n",
      "at point:77, step: 15\n",
      "[29, 13]\n",
      "step: 16\n",
      "at point:13, step: 16\n",
      "[77, 29, 20, 91]\n",
      "at point:20, step: 17\n",
      "[29, 13, 91, 23]\n",
      "at point:23, step: 18\n",
      "[20, 91]\n",
      "step: 19\n",
      "step: 19\n",
      "step: 19\n",
      "step: 19\n",
      "step: 19\n",
      "at point:15, step: 19\n",
      "[97, 38, 41, 86]\n",
      "step: 20\n",
      "step: 20\n",
      "step: 20\n",
      "step: 20\n",
      "step: 20\n",
      "step: 20\n",
      "step: 20\n",
      "step: 20\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 30800 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "ndata = 100; seed= 13013\n",
    "np.random.seed(seed=seed)\n",
    "index = np.arange(0,ndata,1); loc = np.random.rand(ndata,2)*1000; lab = np.zeros(ndata)\n",
    "visited = np.zeros(ndata)\n",
    "KDtree = spatial.cKDTree(loc)\n",
    "plt.scatter(loc[:,0],loc[:,1],marker = 'o',s=10,c='white',edgecolor='black',cmap=plt.cm.inferno)\n",
    "\n",
    "i = 7; radius = 100; minpts = 4\n",
    "\n",
    "lab = np.zeros(ndata)\n",
    "\n",
    "step = 0; maxstep = 20\n",
    "\n",
    "def search(i,loc,lab,radius,minpts,direct,iprev,step,maxstep):\n",
    "    if visited[i] == 0 and step < maxstep:\n",
    "        print('at point:' + str(i) + ', step: ' + str(step))\n",
    "        visited[i] = 1\n",
    "        l = KDtree.query_ball_point(loc[i], radius)\n",
    "        #print('at: ' + str(i) + ', found ' + str(len(l)) + ' points')\n",
    "        l.remove(i)\n",
    "        print(l)\n",
    "        step = step + 1\n",
    "        if len(l) + 1 >= minpts:\n",
    "            lab[i] = 2\n",
    "            plt.scatter(loc[i,0],loc[i,1],s=40,marker='s',c='red',edgecolor='black',cmap=plt.cm.inferno,zorder=100)\n",
    "            for il in l:\n",
    "                plt.plot([loc[i,0],loc[il,0]],[loc[i,1],loc[il,1]],color='grey',alpha=0.7,lw=1,zorder=15)\n",
    "            \n",
    "            if direct == 0:\n",
    "                plt.plot([loc[i,0],loc[iprev,0]],[loc[i,1],loc[iprev,1]],color='black',lw=4.0,zorder=10)\n",
    "                plt.plot([loc[i,0],loc[iprev,0]],[loc[i,1],loc[iprev,1]],color='white',lw=2.0,zorder=13)\n",
    "            #print('anchor: ' + str(i))\n",
    "            #print(l)\n",
    "            for ii in l:\n",
    "                if step < maxstep:\n",
    "                    step = search(ii,loc,lab,radius,minpts,0,i,step,maxstep)\n",
    "        \n",
    "        else:\n",
    "            if direct == 0:\n",
    "                lab[i] = 1\n",
    "                plt.scatter(loc[i,0],loc[i,1],s=30,c='yellow',edgecolor='black',cmap=plt.cm.inferno,zorder=100)\n",
    "            elif direct == 1:\n",
    "                plt.scatter(loc[i,0],loc[i,1],marker='x',s=30,c='black',cmap=plt.cm.inferno)\n",
    "        print('step: ' + str(step))\n",
    "    return step\n",
    "            \n",
    "while np.sum(visited == 0) > 0 and step < maxstep:\n",
    "    #print('unvisited remaining: ' + str(np.sum(visited == 0)))\n",
    "    i = np.random.choice(index[visited == 0],replace=False)  \n",
    "    step = search(i,loc,lab,radius,minpts,1,-1,step,maxstep)\n",
    "        \n",
    "plt.xlim([0,1000]); plt.ylim([0,1000]);plt.xlabel('X (m)');plt.ylabel('Y (m)');plt.title('Points');add_grid()\n",
    "plt.subplots_adjust(left=0.0,bottom=0.0,right=1.0,top=1.1); plt.show() # set plot size  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6e02a56a",
   "metadata": {},
   "source": [
    "### Interactive BDSCAN Dashboard\n",
    "\n",
    "The following code includes:\n",
    "\n",
    "* the dashboard with widgets linked to run DBSCAN over a set number of steps for visualization."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "ce02c52c",
   "metadata": {},
   "outputs": [],
   "source": [
    "l = widgets.Text(value='                                                              Interactive DBSCAN Demo, Prof. Michael Pyrcz, The University of Texas at Austin',\n",
    "                 layout=Layout(width='890px', height='30px'))\n",
    "\n",
    "maxstep = widgets.IntSlider(min=1, max = 101, value=10, step = 1, description = '$maxstep$',orientation='horizontal', style = {'description_width': 'initial'}, continuous_update=False)\n",
    "radius = widgets.FloatSlider(min=10, max = 500, value=110, step = 10, description = r'$r$',orientation='horizontal', style = {'description_width': 'initial'}, continuous_update=False)\n",
    "minpts = widgets.IntSlider(min=2, max = 20, value=4, step = 1, description = r'$min_{pts}$',orientation='horizontal', style = {'description_width': 'initial'}, continuous_update=False)\n",
    "\n",
    "ui = widgets.HBox([maxstep,radius,minpts],)\n",
    "ui2 = widgets.VBox([l,ui],)\n",
    "\n",
    "def run_plot(maxstep,radius,minpts):\n",
    "    ndata = 100; seed= 13013\n",
    "    np.random.seed(seed=seed)\n",
    "    index = np.arange(0,ndata,1); loc = np.random.rand(ndata,2)*1000; lab = np.zeros(ndata)\n",
    "    visited = np.zeros(ndata)\n",
    "    KDtree = spatial.cKDTree(loc)\n",
    "    plt.scatter(loc[:,0],loc[:,1],marker = 'o',s=10,c='white',edgecolor='black',cmap=plt.cm.inferno)\n",
    "    \n",
    "    step = 0\n",
    "    def search(i,loc,lab,radius,minpts,direct,iprev,step,maxstep):\n",
    "        if visited[i] == 0 and step < maxstep:\n",
    "#             print('at point:' + str(i) + ', step: ' + str(step))\n",
    "            visited[i] = 1\n",
    "            l = KDtree.query_ball_point(loc[i], radius)\n",
    "            #print('at: ' + str(i) + ', found ' + str(len(l)) + ' points')\n",
    "            l.remove(i)\n",
    "#             print(l)\n",
    "            step = step + 1            \n",
    "            lab[i] = 2\n",
    "            if len(l) + 1 >= minpts:\n",
    "                plt.scatter(loc[i,0],loc[i,1],s=40,marker='s',c='red',edgecolor='black',cmap=plt.cm.inferno,zorder=100)\n",
    "                \n",
    "                if step > maxstep - 1:\n",
    "                    for il in l:\n",
    "                        plt.plot([loc[i,0],loc[il,0]],[loc[i,1],loc[il,1]],color='black',alpha=0.9,lw=1,zorder=15)\n",
    "                    plt.gca().add_patch(plt.Circle(( loc[i,0] , loc[i,1] ), radius,color='red',fill=False))   \n",
    "                \n",
    "                if direct == 0:\n",
    "                    plt.plot([loc[i,0],loc[iprev,0]],[loc[i,1],loc[iprev,1]],color='black',lw=4.0,zorder=10)\n",
    "                    plt.plot([loc[i,0],loc[iprev,0]],[loc[i,1],loc[iprev,1]],color='white',lw=2.0,zorder=13)\n",
    "                #print('anchor: ' + str(i))\n",
    "                #print(l)\n",
    "                for ii in l:\n",
    "                    if step < maxstep:\n",
    "                        step = search(ii,loc,lab,radius,minpts,0,i,step,maxstep)\n",
    "            \n",
    "            else:\n",
    "                if direct == 0:\n",
    "                    lab[i] = 1\n",
    "                    plt.scatter(loc[i,0],loc[i,1],s=30,c='yellow',edgecolor='black',cmap=plt.cm.inferno,zorder=100)\n",
    "                    plt.plot([loc[i,0],loc[iprev,0]],[loc[i,1],loc[iprev,1]],color='grey',alpha=0.7,lw=1,zorder=15)\n",
    "                elif direct == 1:\n",
    "                    plt.scatter(loc[i,0],loc[i,1],marker='x',s=30,c='black',cmap=plt.cm.inferno)\n",
    "#             print('step: ' + str(step))\n",
    "        return step\n",
    "                \n",
    "    while np.sum(visited == 0) > 0 and step < maxstep:\n",
    "        #print('unvisited remaining: ' + str(np.sum(visited == 0)))\n",
    "        i = np.random.choice(index[visited == 0],replace=False)  \n",
    "        step = search(i,loc,lab,radius,minpts,1,-1,step,maxstep)\n",
    "            \n",
    "    plt.xlim([0,1000]); plt.ylim([0,1000]);plt.xlabel('$X_1$');plt.ylabel('$X_2$');plt.title('Interactive Density-based Clustering with BDSCAN');add_grid()\n",
    "    plt.subplots_adjust(left=0.0,bottom=0.0,right=2.0,top=2.9); 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, {'maxstep':maxstep,'radius':radius,'minpts':minpts})\n",
    "interactive_plot.clear_output(wait = True)               # reduce flickering by delaying plot updating  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "70ea3f08",
   "metadata": {},
   "source": [
    "### Interactive BDSCAN Demonstation \n",
    "\n",
    "#### Michael Pyrcz, Professor, The University of Texas at Austin \n",
    "\n",
    "Set the radius and minimum number of points and step through the recursive BDSCAN method\n",
    "\n",
    "### The Inputs\n",
    "\n",
    "* **maxstep** - recursive steps, **radius** - local neighbourhood, **$min_{pts}$** - minimum number of points"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "54e3b1af",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "0f6df612647b43c6b065a83077e2511c",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(Text(value='                                                              Interactive DBSCAN De…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "abff09c9463f4c6681b0a6ed4d56a7a2",
       "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": "5c1792a3",
   "metadata": {},
   "source": [
    "#### Comments\n",
    "\n",
    "This was a basic demonstration density-based clustering with BDSCAN. 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": "ce851a64",
   "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
}
