{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Basic usage of the cnn module"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "toc": true
   },
   "source": [
    "<h1>Contents<span class=\"tocSkip\"></span></h1>\n",
    "<div class=\"toc\"><ul class=\"toc-item\"><li><span><a href=\"#Pre-requirements\" data-toc-modified-id=\"Pre-requirements-1\"><span class=\"toc-item-num\">1&nbsp;&nbsp;</span>Pre-requirements</a></span><ul class=\"toc-item\"><li><span><a href=\"#Import-dependencies\" data-toc-modified-id=\"Import-dependencies-1.1\"><span class=\"toc-item-num\">1.1&nbsp;&nbsp;</span>Import dependencies</a></span></li><li><span><a href=\"#Notebook-configuration\" data-toc-modified-id=\"Notebook-configuration-1.2\"><span class=\"toc-item-num\">1.2&nbsp;&nbsp;</span>Notebook configuration</a></span></li><li><span><a href=\"#Package-configuration\" data-toc-modified-id=\"Package-configuration-1.3\"><span class=\"toc-item-num\">1.3&nbsp;&nbsp;</span>Package configuration</a></span></li></ul></li><li><span><a href=\"#Getting-started\" data-toc-modified-id=\"Getting-started-2\"><span class=\"toc-item-num\">2&nbsp;&nbsp;</span>Getting started</a></span><ul class=\"toc-item\"><li><span><a href=\"#Class-API\" data-toc-modified-id=\"Class-API-2.1\"><span class=\"toc-item-num\">2.1&nbsp;&nbsp;</span>Class API</a></span></li><li><span><a href=\"#Functional-API\" data-toc-modified-id=\"Functional-API-2.2\"><span class=\"toc-item-num\">2.2&nbsp;&nbsp;</span>Functional API</a></span></li></ul></li></ul></div>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pre-requirements"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Import dependencies"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:21.731048Z",
     "start_time": "2020-06-10T14:08:20.956678Z"
    }
   },
   "outputs": [],
   "source": [
    "import sys\n",
    "\n",
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "import pandas as pd  # Optional dependency\n",
    "\n",
    "import cnnclustering.cnn as cnn  # CNN clustering"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This notebook was created using Python 3.8."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:21.738573Z",
     "start_time": "2020-06-10T14:08:21.734435Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "3.8.3 (default, May 15 2020, 15:24:35) \n",
      "[GCC 8.3.0]\n"
     ]
    }
   ],
   "source": [
    "# Version information\n",
    "print(sys.version)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Notebook configuration"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We use `matplotlib` to create plots. A `\"matplotlibrc\"` file (as provided as `\"${CNNCLUSTERING_DIR}/docs/tutorial/matplotlibrc\"`) can be used to customise the appearance of the plots."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:21.764830Z",
     "start_time": "2020-06-10T14:08:21.741591Z"
    }
   },
   "outputs": [],
   "source": [
    "# Matplotlib configuration\n",
    "mpl.rc_file(\n",
    "    \"matplotlibrc\",\n",
    "    use_default_template=False\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:21.785980Z",
     "start_time": "2020-06-10T14:08:21.773031Z"
    }
   },
   "outputs": [],
   "source": [
    "# Axis property defaults for the plots\n",
    "ax_props = {\n",
    "    \"aspect\": \"equal\"\n",
    "}\n",
    "\n",
    "dot_props = {\n",
    "    \"marker\": \"o\",\n",
    "    \"markeredgecolor\": \"k\"\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Optionally, we can interface with Pandas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:21.805448Z",
     "start_time": "2020-06-10T14:08:21.789277Z"
    }
   },
   "outputs": [],
   "source": [
    "# Pandas DataFrame print options\n",
    "pd.set_option('display.max_rows', 1000)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Package configuration"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `cnnclustering` Python package offers basic options to be configured by the user. On import, a configuration file (`\".cnnclusteringrc\"`), located either in the current working directory or the users home, is loaded. If no such file is provided, the package uses a set of reasonable defaults. An example configuration file can be found under `${CNNCLUSTERING_DIR}/`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:21.826223Z",
     "start_time": "2020-06-10T14:08:21.811519Z"
    }
   },
   "outputs": [],
   "source": [
    "# Configuration file found?\n",
    "cnn.settings.cfgfile  # If None, no file is provided"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:21.857849Z",
     "start_time": "2020-06-10T14:08:21.832423Z"
    },
    "run_control": {
     "marked": true
    },
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'default_cnn_cutoff': '1',\n",
       " 'default_cnn_offset': '0',\n",
       " 'default_radius_cutoff': '1',\n",
       " 'default_member_cutoff': '2',\n",
       " 'default_fit_policy': 'conservative',\n",
       " 'float_precision': 'sp',\n",
       " 'int_precision': 'sp'}"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Display default settings\n",
    "cnn.settings.defaults"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Getting started"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Class API"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `cnnclustering.cnn` module provides a user API through the `CNN` clustering class. An instance of this class can be used to bundle input data (e.g. data points) with cluster results (labels, summary) alongside the clustering methods and convenience functions for further analysis (not only in an Molecular Dynamics context). As a guiding principle, a `CNN` cluster object is always associated with one particular data set and allows varying cluster parameters."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<div class=\"alert alert-info\">\n",
    "\n",
    "**Info:** See also this __scikit-learn__ [fork](https://github.com/janjoswig/scikit-learn) for an alternative API following a parameter centered approach as `sklearn.cluster.cnn`.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A clustering using the class API from the `cnnclustering.cnn` module starts with the initialisation of a clustering object:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:21.875709Z",
     "start_time": "2020-06-10T14:08:21.863674Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================================================================\n",
      "CNN cluster object\n",
      "--------------------------------------------------------------------------------\n",
      "Alias :                         root\n",
      "Hierachy level :                0\n",
      "\n",
      "Data point shape :              Parts      - None\n",
      "                                Points     - None\n",
      "                                Dimensions - None\n",
      "\n",
      "Distance matrix calculated :    None\n",
      "Neighbourhoods calculated :     None\n",
      "Density graph calculated :      None\n",
      "\n",
      "Clustered :                     False\n",
      "Children :                      False\n",
      "================================================================================\n",
      "\n"
     ]
    }
   ],
   "source": [
    "cnnobj = cnn.CNN()  # Create an empty cluster object\n",
    "print(cnnobj)       # Print a cluster object overview"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "At any time it can be useful to get an overview over the current state of a clustering, i.e. which data is associated with a cluster object. From top to bottom the information printed above reads:\n",
    "\n",
    " - `Alias`: An optional string description of the object. This can be useful if many objects are handled at the same time.\n",
    " \n",
    " - `Hierarchy level`: We created this object from scratch, so it will be on the highest possible hierarchy level (0). Dealing with cluster object hierarchies will be described in another tutorial.\n",
    " \n",
    " - `Data point shape`: Data can be associated to the object in the form of data points ($n$ points in $d$ arbitrary dimensions). Data points constitute a primary input format.\n",
    "   - `Parts`: Data point can be organised in independent parts. We will see when this is useful in another tutorial.\n",
    "   - `Points`: Number of points in the data set.\n",
    "   - `Dimensions`: Number of dimension of the data set.\n",
    "   \n",
    " - `Distance matrix calculated`: Data can also (additionally or exclusively) be associated to the object in form of pairwise distances ($n^2$ distances for $n$ data points). Pairwise distances constitute a primary input format. The number of parts underlying the data set is displayed under `Data point shape` even if no points have been loaded explicitly into the object.\n",
    " \n",
    " - `Neighbourhoods calculated`: The CNN clustering relies on point neighbourhood information that has to be calculated with respect to a search radius $r$. Neighbourhoods constitute a secondary input format as they need to be recomputed (from points or distances) for varying values of $r$.\n",
    " \n",
    " - `Density graph calculated`: Similar to neighbourhoods data can be associated to the object in form of a density connectivity graph with respect to neighbour search radius $r$ and a similarity cutoff $c$. Density graphs constitute a secondary input format as they need to be recomputed (from points, distances or neighbourhoods) for varying values of $r$ and $c$.\n",
    " \n",
    " - `Clustered`: Indicates if the data has been cluster, i.e. if label assignments are present.\n",
    " \n",
    " - `Children`: Indicates if cluster objects lower in the cluster hierarchy are linked to the object. Dealing with cluster object hierarchies will be described in another tutorial."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Data can be passed to the clustering object on initialisation as such (see the tutorial on __Data input formats__ an overview on how to pass data of differing nature):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:21.893872Z",
     "start_time": "2020-06-10T14:08:21.879177Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================================================================\n",
      "CNN cluster object\n",
      "--------------------------------------------------------------------------------\n",
      "Alias :                         root\n",
      "Hierachy level :                0\n",
      "\n",
      "Data point shape :              Parts      - 1\n",
      "                                Points     - 12\n",
      "                                Dimensions - 2\n",
      "\n",
      "Distance matrix calculated :    None\n",
      "Neighbourhoods calculated :     None\n",
      "Density graph calculated :      None\n",
      "\n",
      "Clustered :                     False\n",
      "Children :                      False\n",
      "================================================================================\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# 2D data points (list of lists, 12 points in 2 dimensions)\n",
    "data_points = [   # point index\n",
    "    [0, 0],       # 0\n",
    "    [1, 1],       # 1\n",
    "    [1, 0],       # 2\n",
    "    [0, -1],      # 3\n",
    "    [0.5, -0.5],  # 4\n",
    "    [2,  1.5],    # 5\n",
    "    [2.5, -0.5],  # 6\n",
    "    [4, 2],       # 7\n",
    "    [4.5, 2.5],   # 8\n",
    "    [5, -1],      # 9\n",
    "    [5.5, -0.5],  # 10\n",
    "    [5.5, -1.5],  # 11\n",
    "    ]\n",
    "\n",
    "cnnobj = cnn.CNN(points=data_points)\n",
    "print(cnnobj)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We see in the overview that we have now a data set of 12 points in 2 dimensions loaded into the cluster object. The data points are stored on the clustering object under `cnnobj.data`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:21.924442Z",
     "start_time": "2020-06-10T14:08:21.896014Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Points([[ 0. ,  0. ],\n",
       "        [ 1. ,  1. ],\n",
       "        [ 1. ,  0. ],\n",
       "        [ 0. , -1. ],\n",
       "        [ 0.5, -0.5],\n",
       "        [ 2. ,  1.5],\n",
       "        [ 2.5, -0.5],\n",
       "        [ 4. ,  2. ],\n",
       "        [ 4.5,  2.5],\n",
       "        [ 5. , -1. ],\n",
       "        [ 5.5, -0.5],\n",
       "        [ 5.5, -1.5]])"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "cnnobj.data.points"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To cluster these points we can use the `cnnobj.fit` method. The clustering depends on two parameters:\n",
    "  \n",
    "  - `radius_cutoff`: Points are considered neighbours if the distance between them is smaller than this cutoff radius $r$.\n",
    "  - `cnn_cutoff`: Points are assigned to the same cluster if they share at least this number of $c$ common neighbours.\n",
    "  \n",
    "The `cnnobj.fit` method will figure out which kind of input data is provided. For the clustering procedure, we ultimately need to compute the neighbouring points with respect to the `radius_cutoff` for each point in the data set. Based on the `policy` keyword argument, these neighbours are either pre-computed in bulk (`policy = \"progressive\"`) before the actual clustering or on-the-fly during the clustering (`policy = \"conservative\"`).  The current default value for the policy is set to `\"conservative\"`. This can be changed in a configuration file with the option `\"default_fit_policy\"`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:21.943519Z",
     "start_time": "2020-06-10T14:08:21.927207Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Execution time for call of fit: 0 hours, 0 minutes, 0.0009 seconds\n",
      "--------------------------------------------------------------------------------\n",
      "#points   R         C         min       max       #clusters %largest  %noise    \n",
      "12        1.500     1         2         None      2         0.417     0.333     \n",
      "--------------------------------------------------------------------------------\n"
     ]
    }
   ],
   "source": [
    "# Compute neighbourhoods on-the-fly\n",
    "cnnobj.fit(radius_cutoff=1.5, cnn_cutoff=1, policy=\"conservative\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:21.975763Z",
     "start_time": "2020-06-10T14:08:21.946042Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Execution time for call of fit: 0 hours, 0 minutes, 0.0015 seconds\n",
      "--------------------------------------------------------------------------------\n",
      "#points   R         C         min       max       #clusters %largest  %noise    \n",
      "12        1.500     1         2         None      2         0.417     0.333     \n",
      "--------------------------------------------------------------------------------\n"
     ]
    }
   ],
   "source": [
    "# Pre-compute neighbourhoods in bulk\n",
    "cnnobj.fit(radius_cutoff=1.5, cnn_cutoff=1, policy=\"progressive\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<div class=\"alert alert-info\">\n",
    "\n",
    "**Info:** For this simple small data set there is virtually no performance difference between the policies. In general, bulk computation can be computationally more efficient as we can easily make use of efficient external neighbour-search methods. It also allows us to recycle once computed neighbourhood information for cluster approaches with varying `cnn_cutoff` but same `radius_cutoff`. On the other hand it can be quite memory-intensive for large data sets to store the neighbourhood information alongside the data in the original format. Computing the neighbourhoods on-the-fly as we go through the data set is memory friendly but can be computationally more expensive as we fall back to brute-force neighbour-search and once computed information can not be re-used (although this is partly countered by caching). Instead of relying on the specific methods used with the *progressive* policy, the user is also encouraged to pre-compute input with any external method of choice as needed. The possibilities of calculating neighbourhood or distance information are vast and it is wise to choose the appropriate technique for the particular data at hand.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Per default a clustering attempt returns and prints a comprehensive summary of the cluster parameters and the outcome. You can suppress the recording with the keyword argument `rec = False` and the printing with `v = False`:\n",
    "\n",
    "  - N: Number of data points.\n",
    "  - R: Radius cutoff *r*.\n",
    "  - C: CNN cutoff *c*.\n",
    "  - min: Member cutoff (valid clusters need to have at least this many members).\n",
    "  - max: Maximum cluster count (keep only the *max* largest clusters and disregard smaller clusters).\n",
    "  - #clusters: Number of identified clusters.\n",
    "  - %largest: Member share on the total number of points in the largest cluster.\n",
    "  - %noise: Member share on the total number of points identified as noise (not part of any cluster).\n",
    "  \n",
    "The `min` (keyword argument `member_cutoff`) and `max` (keyword argument `max_clusters`) only take effect in an optional post processing step when `sort_by_size = True`. The clusters are sorted in order by there size (first cluster has the highest member count) and are trimmed in the way that small clusters are considered noise."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "When we now look again at the cluster object overview, we can see that two things have changed. First, since we used the *progressive* policy above, the `Neighbourhoods calculated` entry now shows us, that neighbourhoods for 12 points at a search radius of $r = 1.5$ are associated with our object. They can be re-used later.\n",
    "Second, `Clustered` is now set to `True` indicating that we have attempted to cluster the data set."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:21.997943Z",
     "start_time": "2020-06-10T14:08:21.977957Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================================================================\n",
      "CNN cluster object\n",
      "--------------------------------------------------------------------------------\n",
      "Alias :                         root\n",
      "Hierachy level :                0\n",
      "\n",
      "Data point shape :              Parts      - 1\n",
      "                                Points     - 12\n",
      "                                Dimensions - 2\n",
      "\n",
      "Distance matrix calculated :    None\n",
      "Neighbourhoods calculated :     12, r = 1.5\n",
      "Density graph calculated :      None\n",
      "\n",
      "Clustered :                     True\n",
      "Children :                      False\n",
      "================================================================================\n",
      "\n"
     ]
    }
   ],
   "source": [
    "print(cnnobj)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The essential outcome of the clustering are cluster label assignments for each point. Points classified as *noise* (not part of any cluster) are labeled 0. Integer labels larger than 0 indicate the membership to one of the identified cluster of each point. If clusters where sorted (`sort_by_size = True`), cluster 1 has the highest member count."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:22.011740Z",
     "start_time": "2020-06-10T14:08:22.001961Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Labels([1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 2, 2])"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "cnnobj.labels"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `labels` attribute of a cluster object always holds the result of the latest fit. All cluster results (from fits where `rec = True`) are collected in a summary without storing the actual labels."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:22.028063Z",
     "start_time": "2020-06-10T14:08:22.014786Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CNNRecord(points=12, r=1.5, c=1, min=2, max=None, clusters=Labels(2), largest=0.4166666666666667, noise=0.3333333333333333, time=0.0009095668792724609)\n",
      "CNNRecord(points=12, r=1.5, c=1, min=2, max=None, clusters=Labels(2), largest=0.4166666666666667, noise=0.3333333333333333, time=0.0014908313751220703)\n"
     ]
    }
   ],
   "source": [
    "print(*cnnobj.summary, sep=\"\\n\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you have Pandas installed, the summary can be transformed into a handy `pandas.DataFrame`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:22.091142Z",
     "start_time": "2020-06-10T14:08:22.041812Z"
    }
   },
   "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>points</th>\n",
       "      <th>r</th>\n",
       "      <th>c</th>\n",
       "      <th>min</th>\n",
       "      <th>max</th>\n",
       "      <th>clusters</th>\n",
       "      <th>largest</th>\n",
       "      <th>noise</th>\n",
       "      <th>time</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>12</td>\n",
       "      <td>1.5</td>\n",
       "      <td>1</td>\n",
       "      <td>2</td>\n",
       "      <td>&lt;NA&gt;</td>\n",
       "      <td>2</td>\n",
       "      <td>0.416667</td>\n",
       "      <td>0.333333</td>\n",
       "      <td>0.000910</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>12</td>\n",
       "      <td>1.5</td>\n",
       "      <td>1</td>\n",
       "      <td>2</td>\n",
       "      <td>&lt;NA&gt;</td>\n",
       "      <td>2</td>\n",
       "      <td>0.416667</td>\n",
       "      <td>0.333333</td>\n",
       "      <td>0.001491</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   points    r  c  min   max  clusters   largest     noise      time\n",
       "0      12  1.5  1    2  <NA>         2  0.416667  0.333333  0.000910\n",
       "1      12  1.5  1    2  <NA>         2  0.416667  0.333333  0.001491"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "cnnobj.summary.to_DataFrame()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A cluster object comes with a variety of convenience methods that allow for example a quick look at a plot of data points and a cluster result."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:23.721868Z",
     "start_time": "2020-06-10T14:08:22.093354Z"
    },
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 750x450 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, ax = plt.subplots(1, 2)\n",
    "ax[0].set_title(\"original\")\n",
    "cnnobj.evaluate(ax=ax[0], original=True,\n",
    "                ax_props=ax_props, plot_props=dot_props)\n",
    "ax[1].set_title(\"clustered\")\n",
    "cnnobj.evaluate(ax=ax[1],\n",
    "                ax_props=ax_props, plot_props=dot_props)\n",
    "fig.tight_layout()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Functional API"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As an alternative to the class based API, the `cnnclustering.cnn` module provides a functional API for quick results."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-06-10T14:08:23.755896Z",
     "start_time": "2020-06-10T14:08:23.735110Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Execution time for call of fit: 0 hours, 0 minutes, 0.0004 seconds\n",
      "--------------------------------------------------------------------------------\n",
      "#points   R         C         min       max       #clusters %largest  %noise    \n",
      "12        1.500     1         2         None      2         0.417     0.333     \n",
      "--------------------------------------------------------------------------------\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "Labels([1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 2, 2])"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "labels = cnn.fit(\n",
    "    data=cnn.Points(data_points),\n",
    "    radius_cutoff=1.5,\n",
    "    cnn_cutoff=1,\n",
    "    policy=\"conservative\"\n",
    ")\n",
    "labels"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "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.8.3"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": true,
   "title_cell": "Contents",
   "title_sidebar": "Contents",
   "toc_cell": true,
   "toc_position": {
    "height": "calc(100% - 180px)",
    "left": "10px",
    "top": "150px",
    "width": "164.988px"
   },
   "toc_section_display": true,
   "toc_window_display": true
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
