Repository: pysal/pointpats (language: python)

Issue:
Build the READMErst for Pypi release.



Discussion / hints:

Fix patch:
diff --git a/notebooks/spacetime.ipynb b/notebooks/spacetime.ipynb
new file mode 100644
index 0000000..9ccdc0f
--- /dev/null
+++ b/notebooks/spacetime.ipynb
@@ -0,0 +1,1317 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "df4724c7-7e8a-4410-804a-6f0551aafcbe",
+   "metadata": {},
+   "source": [
+    "# Knox space-time interaction statistic"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "cedb45a1-df8c-4a1d-96b0-b48c92b4f55e",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "%load_ext autoreload\n",
+    "%autoreload 2"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "8e5d40f6-0ee7-4587-940f-4fd3428dd4a8",
+   "metadata": {},
+   "source": [
+    "## Current Implementation (to be deprecated)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "551458d6-3d47-463a-8045-c2fe7574a3c0",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "/home/serge/mambaforge/envs/pointpats/lib/python3.11/site-packages/libpysal/cg/alpha_shapes.py:38: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.\n",
+      "  @jit\n",
+      "/home/serge/mambaforge/envs/pointpats/lib/python3.11/site-packages/libpysal/cg/alpha_shapes.py:164: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.\n",
+      "  @jit\n",
+      "/home/serge/mambaforge/envs/pointpats/lib/python3.11/site-packages/libpysal/cg/alpha_shapes.py:198: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.\n",
+      "  @jit\n",
+      "/home/serge/mambaforge/envs/pointpats/lib/python3.11/site-packages/libpysal/cg/alpha_shapes.py:260: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.\n",
+      "  @jit\n"
+     ]
+    }
+   ],
+   "source": [
+    "import pointpats"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "62f68eb2-cd64-426b-977b-76abd77b2349",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import libpysal"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "5c4b19f6-150a-40c8-8022-aab086332ca6",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "path = libpysal.examples.get_path('burkitt.shp')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "b68cc670-4ee3-4d34-9c32-f1b998b2aef5",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from pointpats import SpaceTimeEvents, knox"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "id": "c6b1ec6b-63a9-43bd-9d19-f02678249722",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "/home/serge/mambaforge/envs/pointpats/lib/python3.11/site-packages/libpysal/cg/shapes.py:103: FutureWarning: Objects based on the `Geometry` class will deprecated and removed in a future version of libpysal.\n",
+      "  warnings.warn(dep_msg, FutureWarning)\n"
+     ]
+    }
+   ],
+   "source": [
+    "events = SpaceTimeEvents(path, 'T')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "id": "20b15e1b-1c37-4e8e-99fc-a65904c1c84a",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "<pointpats.spacetime.SpaceTimeEvents at 0x7f42e2a35090>"
+      ]
+     },
+     "execution_count": 7,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "events"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "id": "195767a0-1381-47ad-92be-6c94c7513969",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "id": "e74a1d48-bb41-48a1-be1d-8a62776235d7",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "\u001b[0;31mSignature:\u001b[0m \u001b[0mknox\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms_coords\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mt_coords\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdelta\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtau\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpermutations\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m99\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdebug\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mFalse\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+       "\u001b[0;31mDocstring:\u001b[0m\n",
+       "Knox test for spatio-temporal interaction. :cite:`Knox:1964`\n",
+       "\n",
+       "Parameters\n",
+       "----------\n",
+       "s_coords        : array\n",
+       "                  (n, 2), spatial coordinates.\n",
+       "t_coords        : array\n",
+       "                  (n, 1), temporal coordinates.\n",
+       "delta           : float\n",
+       "                  threshold for proximity in space.\n",
+       "tau             : float\n",
+       "                  threshold for proximity in time.\n",
+       "permutations    : int, optional\n",
+       "                  the number of permutations used to establish pseudo-\n",
+       "                  significance (the default is 99).\n",
+       "debug           : bool, optional\n",
+       "                  if true, debugging information is printed (the default is\n",
+       "                  False).\n",
+       "\n",
+       "Returns\n",
+       "-------\n",
+       "knox_result     : dictionary\n",
+       "                  contains the statistic (stat) for the test and the\n",
+       "                  associated p-value (pvalue).\n",
+       "stat            : float\n",
+       "                  value of the knox test for the dataset.\n",
+       "pvalue          : float\n",
+       "                  pseudo p-value associated with the statistic.\n",
+       "counts          : int\n",
+       "                  count of space time neighbors.\n",
+       "\n",
+       "Examples\n",
+       "--------\n",
+       ">>> import numpy as np\n",
+       ">>> import libpysal as lps\n",
+       ">>> from pointpats import SpaceTimeEvents, knox\n",
+       "\n",
+       "Read in the example data and create an instance of SpaceTimeEvents.\n",
+       "\n",
+       ">>> path = lps.examples.get_path(\"burkitt.shp\")\n",
+       ">>> events = SpaceTimeEvents(path,'T')\n",
+       "\n",
+       "Set the random seed generator. This is used by the permutation based\n",
+       "inference to replicate the pseudo-significance of our example results -\n",
+       "the end-user will normally omit this step.\n",
+       "\n",
+       ">>> np.random.seed(100)\n",
+       "\n",
+       "Run the Knox test with distance and time thresholds of 20 and 5,\n",
+       "respectively. This counts the events that are closer than 20 units in\n",
+       "space, and 5 units in time.\n",
+       "\n",
+       ">>> result = knox(events.space, events.t, delta=20, tau=5, permutations=99)\n",
+       "\n",
+       "Next, we examine the results. First, we call the statistic from the\n",
+       "results dictionary. This reports that there are 13 events close\n",
+       "in both space and time, according to our threshold definitions.\n",
+       "\n",
+       ">>> result['stat'] == 13\n",
+       "True\n",
+       "\n",
+       "Next, we look at the pseudo-significance of this value, calculated by\n",
+       "permuting the timestamps and rerunning the statistics. In this case,\n",
+       "the results indicate there is likely no space-time interaction between\n",
+       "the events.\n",
+       "\n",
+       ">>> print(\"%2.2f\"%result['pvalue'])\n",
+       "0.17\n",
+       "\u001b[0;31mFile:\u001b[0m      ~/projects/pysal/pointpats/pointpats/spacetime.py\n",
+       "\u001b[0;31mType:\u001b[0m      function"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "knox?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "id": "bdceed0d-1ebf-4a93-b6ff-6557e67fcb27",
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array([[300., 302.],\n",
+       "       [291., 270.],\n",
+       "       [326., 263.],\n",
+       "       [299., 376.],\n",
+       "       [267., 327.],\n",
+       "       [266., 356.],\n",
+       "       [267., 345.],\n",
+       "       [262., 338.],\n",
+       "       [268., 335.],\n",
+       "       [335., 275.],\n",
+       "       [302., 272.],\n",
+       "       [301., 304.],\n",
+       "       [302., 272.],\n",
+       "       [324., 337.],\n",
+       "       [260., 338.],\n",
+       "       [306., 385.],\n",
+       "       [284., 362.],\n",
+       "       [293., 332.],\n",
+       "       [274., 353.],\n",
+       "       [263., 333.],\n",
+       "       [307., 386.],\n",
+       "       [274., 357.],\n",
+       "       [297., 282.],\n",
+       "       [288., 365.],\n",
+       "       [283., 366.],\n",
+       "       [290., 352.],\n",
+       "       [284., 341.],\n",
+       "       [286., 341.],\n",
+       "       [282., 350.],\n",
+       "       [292., 278.],\n",
+       "       [305., 381.],\n",
+       "       [298., 378.],\n",
+       "       [299., 381.],\n",
+       "       [287., 342.],\n",
+       "       [276., 380.],\n",
+       "       [261., 345.],\n",
+       "       [273., 347.],\n",
+       "       [284., 357.],\n",
+       "       [266., 361.],\n",
+       "       [285., 360.],\n",
+       "       [278., 350.],\n",
+       "       [305., 321.],\n",
+       "       [293., 332.],\n",
+       "       [297., 375.],\n",
+       "       [332., 273.],\n",
+       "       [256., 345.],\n",
+       "       [275., 357.],\n",
+       "       [267., 350.],\n",
+       "       [267., 301.],\n",
+       "       [286., 379.],\n",
+       "       [322., 332.],\n",
+       "       [278., 349.],\n",
+       "       [280., 356.],\n",
+       "       [276., 354.],\n",
+       "       [261., 343.],\n",
+       "       [323., 298.],\n",
+       "       [260., 346.],\n",
+       "       [283., 352.],\n",
+       "       [282., 361.],\n",
+       "       [308., 390.],\n",
+       "       [293., 275.],\n",
+       "       [331., 272.],\n",
+       "       [266., 308.],\n",
+       "       [280., 354.],\n",
+       "       [301., 271.],\n",
+       "       [279., 361.],\n",
+       "       [321., 280.],\n",
+       "       [292., 391.],\n",
+       "       [315., 307.],\n",
+       "       [323., 333.],\n",
+       "       [282., 302.],\n",
+       "       [269., 348.],\n",
+       "       [330., 349.],\n",
+       "       [281., 302.],\n",
+       "       [330., 396.],\n",
+       "       [328., 341.],\n",
+       "       [264., 323.],\n",
+       "       [303., 382.],\n",
+       "       [264., 285.],\n",
+       "       [259., 337.],\n",
+       "       [329., 350.],\n",
+       "       [328., 351.],\n",
+       "       [284., 351.],\n",
+       "       [309., 370.],\n",
+       "       [329., 270.],\n",
+       "       [273., 347.],\n",
+       "       [295., 267.],\n",
+       "       [262., 353.],\n",
+       "       [281., 352.],\n",
+       "       [325., 335.],\n",
+       "       [288., 341.],\n",
+       "       [302., 248.],\n",
+       "       [315., 273.],\n",
+       "       [295., 377.],\n",
+       "       [284., 358.],\n",
+       "       [272., 376.],\n",
+       "       [305., 369.],\n",
+       "       [279., 318.],\n",
+       "       [290., 339.],\n",
+       "       [311., 376.],\n",
+       "       [288., 256.],\n",
+       "       [324., 284.],\n",
+       "       [287., 353.],\n",
+       "       [283., 364.],\n",
+       "       [267., 337.],\n",
+       "       [280., 298.],\n",
+       "       [275., 326.],\n",
+       "       [281., 299.],\n",
+       "       [313., 365.],\n",
+       "       [273., 332.],\n",
+       "       [283., 363.],\n",
+       "       [304., 366.],\n",
+       "       [308., 387.],\n",
+       "       [330., 275.],\n",
+       "       [260., 339.],\n",
+       "       [289., 335.],\n",
+       "       [268., 314.],\n",
+       "       [329., 268.],\n",
+       "       [264., 298.],\n",
+       "       [274., 342.],\n",
+       "       [286., 373.],\n",
+       "       [263., 346.],\n",
+       "       [265., 335.],\n",
+       "       [269., 340.],\n",
+       "       [260., 359.],\n",
+       "       [279., 355.],\n",
+       "       [284., 399.],\n",
+       "       [270., 337.],\n",
+       "       [267., 363.],\n",
+       "       [320., 282.],\n",
+       "       [282., 315.],\n",
+       "       [303., 377.],\n",
+       "       [303., 247.],\n",
+       "       [264., 297.],\n",
+       "       [303., 337.],\n",
+       "       [289., 380.],\n",
+       "       [270., 330.],\n",
+       "       [262., 335.],\n",
+       "       [274., 342.],\n",
+       "       [262., 333.],\n",
+       "       [262., 333.],\n",
+       "       [267., 369.],\n",
+       "       [255., 290.],\n",
+       "       [267., 336.],\n",
+       "       [273., 309.],\n",
+       "       [270., 328.],\n",
+       "       [270., 339.],\n",
+       "       [265., 334.],\n",
+       "       [276., 360.],\n",
+       "       [276., 360.],\n",
+       "       [283., 363.],\n",
+       "       [305., 374.],\n",
+       "       [264., 326.],\n",
+       "       [286., 361.],\n",
+       "       [278., 333.],\n",
+       "       [257., 344.],\n",
+       "       [302., 395.],\n",
+       "       [275., 378.],\n",
+       "       [290., 361.],\n",
+       "       [279., 362.],\n",
+       "       [300., 255.],\n",
+       "       [270., 357.],\n",
+       "       [290., 356.],\n",
+       "       [291., 368.],\n",
+       "       [283., 361.],\n",
+       "       [270., 325.],\n",
+       "       [266., 332.],\n",
+       "       [267., 367.],\n",
+       "       [300., 364.],\n",
+       "       [268., 390.],\n",
+       "       [276., 337.],\n",
+       "       [293., 396.],\n",
+       "       [273., 345.],\n",
+       "       [267., 338.],\n",
+       "       [278., 367.],\n",
+       "       [310., 387.],\n",
+       "       [298., 268.],\n",
+       "       [266., 332.],\n",
+       "       [279., 340.],\n",
+       "       [266., 361.],\n",
+       "       [267., 344.],\n",
+       "       [272., 370.],\n",
+       "       [327., 383.],\n",
+       "       [265., 335.],\n",
+       "       [310., 387.],\n",
+       "       [279., 339.],\n",
+       "       [277., 379.],\n",
+       "       [258., 350.]])"
+      ]
+     },
+     "execution_count": 10,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "events.space"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "id": "20f228b7-63cb-439a-9223-08462d6ff5af",
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array([[4.130e+02, 1.000e+00],\n",
+       "       [4.720e+02, 1.000e+00],\n",
+       "       [5.110e+02, 1.000e+00],\n",
+       "       [6.890e+02, 1.000e+00],\n",
+       "       [7.300e+02, 1.000e+00],\n",
+       "       [8.470e+02, 1.000e+00],\n",
+       "       [8.710e+02, 1.000e+00],\n",
+       "       [8.990e+02, 1.000e+00],\n",
+       "       [9.210e+02, 1.000e+00],\n",
+       "       [1.134e+03, 1.000e+00],\n",
+       "       [1.190e+03, 1.000e+00],\n",
+       "       [1.214e+03, 1.000e+00],\n",
+       "       [1.224e+03, 1.000e+00],\n",
+       "       [1.322e+03, 1.000e+00],\n",
+       "       [1.399e+03, 1.000e+00],\n",
+       "       [1.480e+03, 1.000e+00],\n",
+       "       [1.503e+03, 1.000e+00],\n",
+       "       [1.549e+03, 1.000e+00],\n",
+       "       [1.567e+03, 1.000e+00],\n",
+       "       [1.607e+03, 1.000e+00],\n",
+       "       [1.615e+03, 1.000e+00],\n",
+       "       [1.657e+03, 1.000e+00],\n",
+       "       [1.688e+03, 1.000e+00],\n",
+       "       [1.695e+03, 1.000e+00],\n",
+       "       [1.714e+03, 1.000e+00],\n",
+       "       [1.811e+03, 1.000e+00],\n",
+       "       [1.813e+03, 1.000e+00],\n",
+       "       [1.910e+03, 1.000e+00],\n",
+       "       [1.986e+03, 1.000e+00],\n",
+       "       [1.996e+03, 1.000e+00],\n",
+       "       [2.049e+03, 1.000e+00],\n",
+       "       [2.053e+03, 1.000e+00],\n",
+       "       [2.075e+03, 1.000e+00],\n",
+       "       [2.081e+03, 1.000e+00],\n",
+       "       [2.103e+03, 1.000e+00],\n",
+       "       [2.204e+03, 1.000e+00],\n",
+       "       [2.209e+03, 1.000e+00],\n",
+       "       [2.215e+03, 1.000e+00],\n",
+       "       [2.239e+03, 1.000e+00],\n",
+       "       [2.272e+03, 1.000e+00],\n",
+       "       [2.281e+03, 1.000e+00],\n",
+       "       [2.349e+03, 1.000e+00],\n",
+       "       [2.351e+03, 1.000e+00],\n",
+       "       [2.373e+03, 1.000e+00],\n",
+       "       [2.377e+03, 1.000e+00],\n",
+       "       [2.388e+03, 1.000e+00],\n",
+       "       [2.411e+03, 1.000e+00],\n",
+       "       [2.412e+03, 1.000e+00],\n",
+       "       [2.419e+03, 1.000e+00],\n",
+       "       [2.424e+03, 1.000e+00],\n",
+       "       [2.488e+03, 1.000e+00],\n",
+       "       [2.510e+03, 1.000e+00],\n",
+       "       [2.512e+03, 1.000e+00],\n",
+       "       [2.533e+03, 1.000e+00],\n",
+       "       [2.545e+03, 1.000e+00],\n",
+       "       [2.588e+03, 1.000e+00],\n",
+       "       [2.616e+03, 1.000e+00],\n",
+       "       [2.617e+03, 1.000e+00],\n",
+       "       [2.629e+03, 1.000e+00],\n",
+       "       [2.658e+03, 1.000e+00],\n",
+       "       [2.695e+03, 1.000e+00],\n",
+       "       [2.715e+03, 1.000e+00],\n",
+       "       [2.766e+03, 1.000e+00],\n",
+       "       [2.772e+03, 1.000e+00],\n",
+       "       [2.800e+03, 1.000e+00],\n",
+       "       [2.888e+03, 1.000e+00],\n",
+       "       [2.996e+03, 1.000e+00],\n",
+       "       [3.027e+03, 1.000e+00],\n",
+       "       [3.118e+03, 1.000e+00],\n",
+       "       [3.153e+03, 1.000e+00],\n",
+       "       [3.149e+03, 1.000e+00],\n",
+       "       [3.149e+03, 1.000e+00],\n",
+       "       [3.173e+03, 1.000e+00],\n",
+       "       [3.237e+03, 1.000e+00],\n",
+       "       [3.245e+03, 1.000e+00],\n",
+       "       [3.266e+03, 1.000e+00],\n",
+       "       [3.275e+03, 1.000e+00],\n",
+       "       [3.286e+03, 1.000e+00],\n",
+       "       [3.302e+03, 1.000e+00],\n",
+       "       [3.308e+03, 1.000e+00],\n",
+       "       [3.323e+03, 1.000e+00],\n",
+       "       [3.342e+03, 1.000e+00],\n",
+       "       [3.346e+03, 1.000e+00],\n",
+       "       [3.422e+03, 1.000e+00],\n",
+       "       [3.453e+03, 1.000e+00],\n",
+       "       [3.453e+03, 1.000e+00],\n",
+       "       [3.500e+03, 1.000e+00],\n",
+       "       [3.530e+03, 1.000e+00],\n",
+       "       [3.570e+03, 1.000e+00],\n",
+       "       [3.576e+03, 1.000e+00],\n",
+       "       [3.614e+03, 1.000e+00],\n",
+       "       [3.636e+03, 1.000e+00],\n",
+       "       [3.661e+03, 1.000e+00],\n",
+       "       [3.693e+03, 1.000e+00],\n",
+       "       [3.716e+03, 1.000e+00],\n",
+       "       [3.773e+03, 1.000e+00],\n",
+       "       [3.790e+03, 1.000e+00],\n",
+       "       [3.806e+03, 1.000e+00],\n",
+       "       [3.843e+03, 1.000e+00],\n",
+       "       [3.848e+03, 1.000e+00],\n",
+       "       [3.848e+03, 1.000e+00],\n",
+       "       [3.851e+03, 1.000e+00],\n",
+       "       [3.973e+03, 1.000e+00],\n",
+       "       [3.976e+03, 1.000e+00],\n",
+       "       [3.878e+03, 1.000e+00],\n",
+       "       [3.963e+03, 1.000e+00],\n",
+       "       [3.985e+03, 1.000e+00],\n",
+       "       [4.079e+03, 1.000e+00],\n",
+       "       [4.080e+03, 1.000e+00],\n",
+       "       [4.107e+03, 1.000e+00],\n",
+       "       [4.118e+03, 1.000e+00],\n",
+       "       [4.133e+03, 1.000e+00],\n",
+       "       [4.156e+03, 1.000e+00],\n",
+       "       [4.194e+03, 1.000e+00],\n",
+       "       [4.195e+03, 1.000e+00],\n",
+       "       [4.207e+03, 1.000e+00],\n",
+       "       [4.212e+03, 1.000e+00],\n",
+       "       [4.213e+03, 1.000e+00],\n",
+       "       [4.213e+03, 1.000e+00],\n",
+       "       [4.216e+03, 1.000e+00],\n",
+       "       [4.235e+03, 1.000e+00],\n",
+       "       [4.258e+03, 1.000e+00],\n",
+       "       [4.273e+03, 1.000e+00],\n",
+       "       [4.277e+03, 1.000e+00],\n",
+       "       [4.290e+03, 1.000e+00],\n",
+       "       [4.336e+03, 1.000e+00],\n",
+       "       [4.339e+03, 1.000e+00],\n",
+       "       [4.352e+03, 1.000e+00],\n",
+       "       [4.383e+03, 1.000e+00],\n",
+       "       [4.385e+03, 1.000e+00],\n",
+       "       [4.441e+03, 1.000e+00],\n",
+       "       [4.492e+03, 1.000e+00],\n",
+       "       [4.518e+03, 1.000e+00],\n",
+       "       [4.554e+03, 1.000e+00],\n",
+       "       [4.565e+03, 1.000e+00],\n",
+       "       [4.610e+03, 1.000e+00],\n",
+       "       [4.628e+03, 1.000e+00],\n",
+       "       [4.637e+03, 1.000e+00],\n",
+       "       [4.638e+03, 1.000e+00],\n",
+       "       [4.700e+03, 1.000e+00],\n",
+       "       [4.700e+03, 1.000e+00],\n",
+       "       [4.701e+03, 1.000e+00],\n",
+       "       [4.708e+03, 1.000e+00],\n",
+       "       [4.750e+03, 1.000e+00],\n",
+       "       [4.751e+03, 1.000e+00],\n",
+       "       [4.764e+03, 1.000e+00],\n",
+       "       [4.780e+03, 1.000e+00],\n",
+       "       [4.806e+03, 1.000e+00],\n",
+       "       [4.822e+03, 1.000e+00],\n",
+       "       [4.848e+03, 1.000e+00],\n",
+       "       [4.858e+03, 1.000e+00],\n",
+       "       [4.861e+03, 1.000e+00],\n",
+       "       [4.862e+03, 1.000e+00],\n",
+       "       [4.888e+03, 1.000e+00],\n",
+       "       [4.891e+03, 1.000e+00],\n",
+       "       [4.914e+03, 1.000e+00],\n",
+       "       [4.918e+03, 1.000e+00],\n",
+       "       [4.932e+03, 1.000e+00],\n",
+       "       [4.944e+03, 1.000e+00],\n",
+       "       [4.948e+03, 1.000e+00],\n",
+       "       [4.964e+03, 1.000e+00],\n",
+       "       [4.975e+03, 1.000e+00],\n",
+       "       [5.001e+03, 1.000e+00],\n",
+       "       [5.002e+03, 1.000e+00],\n",
+       "       [5.072e+03, 1.000e+00],\n",
+       "       [5.135e+03, 1.000e+00],\n",
+       "       [5.199e+03, 1.000e+00],\n",
+       "       [5.353e+03, 1.000e+00],\n",
+       "       [5.358e+03, 1.000e+00],\n",
+       "       [5.361e+03, 1.000e+00],\n",
+       "       [5.489e+03, 1.000e+00],\n",
+       "       [5.495e+03, 1.000e+00],\n",
+       "       [5.505e+03, 1.000e+00],\n",
+       "       [5.530e+03, 1.000e+00],\n",
+       "       [5.578e+03, 1.000e+00],\n",
+       "       [5.583e+03, 1.000e+00],\n",
+       "       [5.588e+03, 1.000e+00],\n",
+       "       [5.599e+03, 1.000e+00],\n",
+       "       [5.641e+03, 1.000e+00],\n",
+       "       [5.643e+03, 1.000e+00],\n",
+       "       [5.650e+03, 1.000e+00],\n",
+       "       [5.661e+03, 1.000e+00],\n",
+       "       [5.702e+03, 1.000e+00],\n",
+       "       [5.728e+03, 1.000e+00],\n",
+       "       [5.752e+03, 1.000e+00],\n",
+       "       [5.753e+03, 1.000e+00],\n",
+       "       [5.755e+03, 1.000e+00],\n",
+       "       [5.775e+03, 1.000e+00]])"
+      ]
+     },
+     "execution_count": 11,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "events.time"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "id": "fcc2c653-f234-46ce-bbd9-a13fa01d977d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "numpy.random.seed(100)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "id": "e0422470-af63-47a0-ba5b-40d0e2975824",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "/tmp/ipykernel_3204526/489981337.py:1: DeprecationWarning: This function is deprecated. Use Knox\n",
+      "  result = knox(events.space, events.t, delta=20, tau=5, permutations=99)\n"
+     ]
+    }
+   ],
+   "source": [
+    "result = knox(events.space, events.t, delta=20, tau=5, permutations=99)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "id": "2b7576e8-42dc-4fed-841f-bab6d105d557",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'stat': 13, 'pvalue': array([0.17])}"
+      ]
+     },
+     "execution_count": 14,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "result"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 15,
+   "id": "4c0fe4ab-8884-4709-9b43-683b1ac14eb0",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "(188, 2)"
+      ]
+     },
+     "execution_count": 15,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "events.space.shape"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "id": "082197b3-a7cc-4829-8e74-045943e28675",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "(188, 1)"
+      ]
+     },
+     "execution_count": 16,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "events.t.shape"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "50ea59b1-d82f-4fbf-97b0-55aac4ead2b4",
+   "metadata": {},
+   "source": [
+    "## New Implementation\n",
+    "\n",
+    "- Global Knox with analytical inference\n",
+    "- Global Knox with space/time distance queries\n",
+    "- Local Knox with analytical inference\n",
+    "- Local Knox with conditional permutations\n",
+    "- Better Reporting"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 17,
+   "id": "4d60c4c9-95ee-42c3-b458-bfce15315292",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import geopandas"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 18,
+   "id": "2653a3e2-af07-431e-9001-2f0d0f55cc9f",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "df = geopandas.read_file(path)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 19,
+   "id": "d44e2752-1711-492c-819e-ce621db4ad70",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>ID</th>\n",
+       "      <th>X</th>\n",
+       "      <th>Y</th>\n",
+       "      <th>T</th>\n",
+       "      <th>AGE</th>\n",
+       "      <th>DATE</th>\n",
+       "      <th>geometry</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>1.0</td>\n",
+       "      <td>300.0</td>\n",
+       "      <td>302.0</td>\n",
+       "      <td>413.0</td>\n",
+       "      <td>22.0</td>\n",
+       "      <td>1901-02-16</td>\n",
+       "      <td>POINT (300.000 302.000)</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>2.0</td>\n",
+       "      <td>291.0</td>\n",
+       "      <td>270.0</td>\n",
+       "      <td>472.0</td>\n",
+       "      <td>5.0</td>\n",
+       "      <td>1901-04-16</td>\n",
+       "      <td>POINT (291.000 270.000)</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>3.0</td>\n",
+       "      <td>326.0</td>\n",
+       "      <td>263.0</td>\n",
+       "      <td>511.0</td>\n",
+       "      <td>12.0</td>\n",
+       "      <td>1901-05-25</td>\n",
+       "      <td>POINT (326.000 263.000)</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>4.0</td>\n",
+       "      <td>299.0</td>\n",
+       "      <td>376.0</td>\n",
+       "      <td>689.0</td>\n",
+       "      <td>6.0</td>\n",
+       "      <td>1901-11-19</td>\n",
+       "      <td>POINT (299.000 376.000)</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>5.0</td>\n",
+       "      <td>267.0</td>\n",
+       "      <td>327.0</td>\n",
+       "      <td>730.0</td>\n",
+       "      <td>4.0</td>\n",
+       "      <td>1901-12-30</td>\n",
+       "      <td>POINT (267.000 327.000)</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "    ID      X      Y      T   AGE        DATE                 geometry\n",
+       "0  1.0  300.0  302.0  413.0  22.0  1901-02-16  POINT (300.000 302.000)\n",
+       "1  2.0  291.0  270.0  472.0   5.0  1901-04-16  POINT (291.000 270.000)\n",
+       "2  3.0  326.0  263.0  511.0  12.0  1901-05-25  POINT (326.000 263.000)\n",
+       "3  4.0  299.0  376.0  689.0   6.0  1901-11-19  POINT (299.000 376.000)\n",
+       "4  5.0  267.0  327.0  730.0   4.0  1901-12-30  POINT (267.000 327.000)"
+      ]
+     },
+     "execution_count": 19,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "df.head()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 20,
+   "id": "10469df1-d859-4652-a0c2-0ee5468110b4",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "<Axes: >"
+      ]
+     },
+     "execution_count": 20,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "image/png": "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",
+      "text/plain": [
+       "<Figure size 640x480 with 1 Axes>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "df.plot()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 21,
+   "id": "8fde7933-82b4-41db-99b0-90bac7481c49",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from pointpats.spacetime import Knox"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 22,
+   "id": "4b5e2057-39b2-4b9b-aafc-e16758558578",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "results = Knox(df[['X', 'Y']], df[[\"T\"]], delta=20, tau=5)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 23,
+   "id": "57c4ea44-538c-42d5-9b04-71e3ccb1619f",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "13"
+      ]
+     },
+     "execution_count": 23,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "results.statistic_"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 24,
+   "id": "48d141cc-b604-419d-bfc5-9ff46696e1d4",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "0.28"
+      ]
+     },
+     "execution_count": 24,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "results.p_sim"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 25,
+   "id": "f8951730-4f33-47f2-8936-d459046b5778",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "0.14624558197140414"
+      ]
+     },
+     "execution_count": 25,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "results.p_poisson"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 26,
+   "id": "7b67a026-0b33-4a20-9394-bc07f8c964e4",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "numpy.random.seed(123456)\n",
+    "results = Knox(df[['X', 'Y']], df[[\"T\"]], delta=20, tau=5,keep=True)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 27,
+   "id": "2b8f3e5c-2fe4-49da-999f-f56002e5a2f4",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array([[1.300e+01, 3.416e+03],\n",
+       "       [3.900e+01, 1.411e+04]])"
+      ]
+     },
+     "execution_count": 27,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "results.observed"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 28,
+   "id": "cc201594-dc3d-40d5-8958-7eea166ca3c7",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array([[1.01438161e+01, 3.41885618e+03],\n",
+       "       [4.18561839e+01, 1.41071438e+04]])"
+      ]
+     },
+     "execution_count": 28,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "results.expected"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 29,
+   "id": "43185747-b6ae-4381-aa22-2401933c8d30",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "True"
+      ]
+     },
+     "execution_count": 29,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "results.keep"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 30,
+   "id": "fc52cc8f-6e23-406b-8f8d-4c9d2b3b5dd5",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "0.24"
+      ]
+     },
+     "execution_count": 30,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "results.p_sim"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 31,
+   "id": "c43b4594-8d3f-4a9b-b10f-0faa7ee44d09",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "results = Knox(df[['X', 'Y']], df[[\"T\"]], delta=20, tau=5, keep=True)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 32,
+   "id": "222cd0e9-2b19-4bd0-a436-a45ed3fa80ce",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "True"
+      ]
+     },
+     "execution_count": 32,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "hasattr(results, \"sim\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 33,
+   "id": "b96c1cfd-1a80-469f-9257-19356109dc70",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array([16., 10., 13.,  7., 10.,  9.,  8., 10., 10.,  9.,  7., 14.,  9.,\n",
+       "       11., 11., 13.,  8.,  9.,  8.,  8.,  8.,  7.,  8., 13.,  8., 12.,\n",
+       "       10., 11.,  9., 12.,  8.,  6.,  9., 11., 12., 16., 14., 11., 13.,\n",
+       "       12., 11.,  8., 13.,  7., 10.,  4.,  8., 14.,  9., 12., 12.,  6.,\n",
+       "        8., 10.,  8.,  9.,  7.,  8., 11., 12., 10., 17., 10.,  8., 12.,\n",
+       "       13.,  9.,  8.,  9., 10., 11.,  6., 10., 13., 17., 11.,  8.,  8.,\n",
+       "       10.,  7., 10.,  9., 13., 10.,  7., 13.,  6.,  9., 11.,  9., 14.,\n",
+       "       14.,  6., 11., 13., 12., 10., 10., 11.])"
+      ]
+     },
+     "execution_count": 33,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "results.sim"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 35,
+   "id": "06145a2b-6c8f-4cc7-abaa-e840cdabb197",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from pointpats.spacetime import KnoxLocal\n",
+    "numpy.random.seed(12345)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 36,
+   "id": "1d8a9059-c1d2-428b-a58a-cdd1658ab150",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "lres = results = KnoxLocal(df[['X', 'Y']], df[[\"T\"]], delta=20, tau=5)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 37,
+   "id": "aff0fa64-dda0-41ca-a614-1254273ed136",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "(188,)"
+      ]
+     },
+     "execution_count": 37,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "lres.statistic_.shape"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 38,
+   "id": "02a8b0ef-daf3-4cd6-afe4-2d0a791a0942",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "(array([ 25,  26,  30,  31,  35,  36,  41,  42,  46,  47,  51,  52, 102,\n",
+       "        103, 116, 118, 122, 123, 137, 138, 139, 140, 158, 159, 162, 163]),)"
+      ]
+     },
+     "execution_count": 38,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "gt0ids = numpy.where(lres.nsti>0)\n",
+    "gt0ids"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 39,
+   "id": "0e9b36af-e7f9-44d5-9c28-e6b9fc49f675",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,\n",
+       "       1., 1., 1., 1., 1., 1., 1., 1., 1.])"
+      ]
+     },
+     "execution_count": 39,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "lres.nsti[gt0ids]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 40,
+   "id": "aa74c51b-2a2d-4992-b244-6031aef4e6af",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array([0.1348993 , 0.14220663, 0.07335085, 0.08400282, 0.1494317 ,\n",
+       "       0.21524073, 0.0175806 , 0.04599869, 0.17523687, 0.18209188,\n",
+       "       0.19111321, 0.16830444, 0.13734428, 0.14703242, 0.06796364,\n",
+       "       0.03192559, 0.13734428, 0.17523687, 0.12998154, 0.1933476 ,\n",
+       "       0.13244507, 0.13244507, 0.12502644, 0.14703242, 0.12502644,\n",
+       "       0.12998154])"
+      ]
+     },
+     "execution_count": 40,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "lres.p_hypergeom[gt0ids]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 41,
+   "id": "f252aad8-e872-4217-9549-c644da41b4c4",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array([0.3 , 0.33, 0.11, 0.17, 0.3 , 0.42, 0.06, 0.06, 0.33, 0.34, 0.36,\n",
+       "       0.38, 0.3 , 0.29, 0.41, 0.19, 0.31, 0.39, 0.18, 0.39, 0.48, 0.41,\n",
+       "       0.22, 0.41, 0.39, 0.32])"
+      ]
+     },
+     "execution_count": 41,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "lres.p_sims[gt0ids]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f6377b09-2b94-4fd3-9f85-9c378c015464",
+   "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
+}
diff --git a/pointpats/spacetime.py b/pointpats/spacetime.py
index 61d7039..567b05c 100644
--- a/pointpats/spacetime.py
+++ b/pointpats/spacetime.py
@@ -2,17 +2,39 @@
 Methods for identifying space-time interaction in spatio-temporal event
 data.
 """
-__author__ = "Nicholas Malizia <nmalizia@asu.edu>", "Sergio J. Rey \
-<srey@asu.edu>", "Philip Stephens <philip.stephens@asu.edu"
-
-__all__ = ['SpaceTimeEvents', 'knox', 'mantel', 'jacquez', 'modified_knox']
+__author__ = (
+    "Eli Knaap <eknaap@sdsu.edu>",
+    "Nicholas Malizia <nmalizia@asu.edu>",
+    "Sergio J. Rey <srey@sdsu.edu>",
+    "Philip Stephens <philip.stephens@asu.edu",
+)
+
+__all__ = [
+    "SpaceTimeEvents",
+    "knox",
+    "mantel",
+    "jacquez",
+    "modified_knox",
+    "Knox",
+    "KnoxLocal",
+]
 
 import os
+from datetime import date
+from functools import cached_property
+from warnings import warn
+
+import geopandas as gpd
 import libpysal as lps
 import numpy as np
+import pandas
 import scipy.stats as stats
 from libpysal import cg
-from datetime import date
+from pandas.api.types import is_numeric_dtype
+from scipy.spatial import KDTree
+from scipy.stats import hypergeom, poisson
+from shapely.geometry import LineString
+
 
 class SpaceTimeEvents:
     """
@@ -113,10 +135,11 @@ class SpaceTimeEvents:
     >>> (events.t[1][0] - events.t[0][0]).days
     59
     """
+
     def __init__(self, path, time_col, infer_timestamp=False):
         shp = lps.io.open(path)
         head, tail = os.path.split(path)
-        dbf_tail = tail.split(".")[0]+".dbf"
+        dbf_tail = tail.split(".")[0] + ".dbf"
         dbf = lps.io.open(lps.examples.get_path(dbf_tail))
 
         # extract the spatial coordinates from the shapefile
@@ -138,8 +161,10 @@ def __init__(self, path, time_col, infer_timestamp=False):
                 col = [(d - day1).days for d in col]
                 t = np.array(col)
             else:
-                print("Unable to parse your time column as Python datetime \
-                      objects, proceeding as integers.")
+                print(
+                    "Unable to parse your time column as Python datetime \
+                      objects, proceeding as integers."
+                )
                 t = np.array(col)
         else:
             t = np.array(dbf.by_col(time_col))
@@ -223,6 +248,7 @@ def knox(s_coords, t_coords, delta, tau, permutations=99, debug=False):
     >>> print("%2.2f"%result['pvalue'])
     0.17
     """
+    warn("This function is deprecated. Use Knox", DeprecationWarning, stacklevel=2)
 
     # Do a kdtree on space first as the number of ties (identical points) is
     # likely to be lower for space than time.
@@ -238,7 +264,7 @@ def knox(s_coords, t_coords, delta, tau, permutations=99, debug=False):
     d_t = (t_coords[ids[:, 0]] - t_coords[ids[:, 1]]) ** 2
     n_st = sum(d_t <= tau2)
 
-    knox_result = {'stat': n_st[0]}
+    knox_result = {"stat": n_st[0]}
 
     if permutations:
         joint = np.zeros((permutations, 1), int)
@@ -250,12 +276,14 @@ def knox(s_coords, t_coords, delta, tau, permutations=99, debug=False):
         larger = sum(joint >= n_st[0])
         if (permutations - larger) < larger:
             larger = permutations - larger
-        p_sim = (larger + 1.) / (permutations + 1.)
-        knox_result['pvalue'] = p_sim
+        p_sim = (larger + 1.0) / (permutations + 1.0)
+        knox_result["pvalue"] = p_sim
     return knox_result
 
 
-def mantel(s_coords, t_coords, permutations=99, scon=1.0, spow=-1.0, tcon=1.0, tpow=-1.0):
+def mantel(
+    s_coords, t_coords, permutations=99, scon=1.0, spow=-1.0, tcon=1.0, tpow=-1.0
+):
     """
     Standardized Mantel test for spatio-temporal interaction. :cite:`Mantel:1967`
 
@@ -339,8 +367,8 @@ def mantel(s_coords, t_coords, permutations=99, scon=1.0, spow=-1.0, tcon=1.0, t
     timemat = cg.distance_matrix(t)
 
     # calculate the transformed standardized statistic
-    timevec = (timemat[np.tril_indices(timemat.shape[0], k = -1)] + tcon) ** tpow
-    distvec = (distmat[np.tril_indices(distmat.shape[0], k = -1)] + scon) ** spow
+    timevec = (timemat[np.tril_indices(timemat.shape[0], k=-1)] + tcon) ** tpow
+    distvec = (distmat[np.tril_indices(distmat.shape[0], k=-1)] + scon) ** spow
     stat = stats.pearsonr(timevec, distvec)[0].sum()
 
     # return the results (if no inference)
@@ -351,7 +379,7 @@ def mantel(s_coords, t_coords, permutations=99, scon=1.0, spow=-1.0, tcon=1.0, t
     dist = []
     for i in range(permutations):
         trand = _shuffle_matrix(timemat, np.arange(n))
-        timevec = (trand[np.tril_indices(trand.shape[0], k = -1)] + tcon) ** tpow
+        timevec = (trand[np.tril_indices(trand.shape[0], k=-1)] + tcon) ** tpow
         m = stats.pearsonr(timevec, distvec)[0].sum()
         dist.append(m)
 
@@ -362,7 +390,7 @@ def mantel(s_coords, t_coords, permutations=99, scon=1.0, spow=-1.0, tcon=1.0, t
     pvalue = (count + 1.0) / (permutations + 1.0)
 
     # report the results
-    mantel_result = {'stat': stat, 'pvalue': pvalue}
+    mantel_result = {"stat": stat, "pvalue": pvalue}
     return mantel_result
 
 
@@ -409,14 +437,14 @@ def jacquez(s_coords, t_coords, k, permutations=99):
     The Jacquez test counts the number of events that are k nearest
     neighbors in both time and space. The following runs the Jacquez test
     on the example data and reports the resulting statistic. In this case,
-    there are 13 instances where events are nearest neighbors in both space
+    there are 12 instances where events are nearest neighbors in both space
     and time.
     # turning off as kdtree changes from scipy < 0.12 return 13
 
     >>> np.random.seed(100)
     >>> result = jacquez(events.space, events.t ,k=3,permutations=99)
     >>> print(result['stat'])
-    13
+    12
 
     The significance of this can be assessed by calling the p-
     value from the results dictionary, as shown below. Again, no
@@ -477,7 +505,7 @@ def jacquez(s_coords, t_coords, k, permutations=99):
     pvalue = (count + 1.0) / (permutations + 1.0)
 
     # report the results
-    jacquez_result = {'stat': stat, 'pvalue': pvalue}
+    jacquez_result = {"stat": stat, "pvalue": pvalue}
     return jacquez_result
 
 
@@ -566,7 +594,7 @@ def modified_knox(s_coords, t_coords, delta, tau, permutations=99):
 
     # calculate the observed (original) statistic
     knoxmat = timemat * spacmat
-    obsstat = (knoxmat.sum() - n)
+    obsstat = knoxmat.sum() - n
 
     # calculate the expectated value
     ssumvec = np.reshape((spacbin.sum(axis=0) - 1), (n, 1))
@@ -590,7 +618,7 @@ def modified_knox(s_coords, t_coords, delta, tau, permutations=99):
 
         # calculate the observed knox again
         knoxmat = timemat * spacmat
-        obsstat = (knoxmat.sum() - n)
+        obsstat = knoxmat.sum() - n
 
         # calculate the expectated value again
         ssumvec = np.reshape((spacbin.sum(axis=0) - 1), (n, 1))
@@ -608,9 +636,10 @@ def modified_knox(s_coords, t_coords, delta, tau, permutations=99):
     pvalue = (count + 1.0) / (permutations + 1.0)
 
     # return results
-    modknox_result = {'stat': stat, 'pvalue': pvalue}
+    modknox_result = {"stat": stat, "pvalue": pvalue}
     return modknox_result
 
+
 def _shuffle_matrix(X, ids):
     """
     Random permutation of rows and columns of a matrix
@@ -629,3 +658,871 @@ def _shuffle_matrix(X, ids):
     """
     np.random.shuffle(ids)
     return X[ids, :][:, ids]
+
+
+def _knox(s_coords, t_coords, delta, tau, permutations=99, keep=False):
+    """
+    Parameters
+    ==========
+
+    s_coords: array-like
+      spatial coordinates
+    t_coords: array-like
+      temporal coordinates
+    delta: float
+      distance threshold
+    tau: float
+      temporal threshold
+    permutations: int
+      number of permutations
+    keep: bool
+      return values from permutations (default False)
+
+
+    Returns
+    =======
+
+    summary table observed
+    summary table h0
+
+    ns
+    nt
+    nst
+    n
+    p-value
+    """
+
+    n = s_coords.shape[0]
+
+    stree = KDTree(s_coords)
+    ttree = KDTree(t_coords)
+    sneighbors = stree.query_ball_tree(stree, r=delta)
+    sneighbors = [
+        set(neighbors).difference([i]) for i, neighbors in enumerate(sneighbors)
+    ]
+    tneighbors = ttree.query_ball_tree(ttree, r=tau)
+    tneighbors = [
+        set(neighbors).difference([i]) for i, neighbors in enumerate(tneighbors)
+    ]
+
+    # number of spatial neighbor pairs
+    ns = np.array([len(neighbors) for neighbors in sneighbors])  # by i
+
+    NS = ns.sum() / 2  # total
+
+    # number of temporal neigbor pairs
+    nt = np.array([len(neighbors) for neighbors in tneighbors])
+    NT = nt.sum() / 2
+
+    # s-t neighbors (list of lists)
+    stneighbors = [
+        sneighbors_i.intersection(tneighbors_i)
+        for sneighbors_i, tneighbors_i in zip(sneighbors, tneighbors)
+    ]
+
+    # number of spatio-temporal neigbor pairs
+    nst = np.array([len(neighbors) for neighbors in stneighbors])
+    NST = nst.sum() / 2
+
+    all_pairs = []
+    pairs = {}
+    for i, neigh in enumerate(stneighbors):
+        if len(neigh) > 0:
+            all_pairs.extend([sorted((i, j)) for j in neigh])
+    st_pairs = set([tuple(l) for l in all_pairs])
+
+    # ENST: expected number of spatio-temporal neighbors under HO
+    pairs = n * (n - 1) / 2
+    ENST = NS * NT / pairs
+
+    # observed table
+    observed = np.zeros((2, 2))
+
+    NS_ = NS - NST  # spatial only
+    NT_ = NT - NST  # temporal only
+
+    observed[0, 0] = NST
+    observed[0, 1] = NS_
+    observed[1, 0] = NT_
+    observed[1, 1] = pairs - NST - NS_ - NT_
+
+    # expected table
+
+    expected = np.zeros((2, 2))
+    expected[0, 0] = ENST
+    expected[0, 1] = NS - expected[0, 0]
+    expected[1, 0] = NT - expected[0, 0]
+    expected[1, 1] = pairs - expected.sum()
+
+    p_value_poisson = 1 - poisson.cdf(NST, expected[0, 0])
+
+    results = {}
+    results["ns"] = ns.sum() / 2
+    results["nt"] = nt.sum() / 2
+    results["nst"] = nst.sum() / 2
+    results["pairs"] = pairs
+    results["expected"] = expected
+    results["observed"] = observed
+    results["p_value_poisson"] = p_value_poisson
+    results["st_pairs"] = st_pairs
+    results["sneighbors"] = sneighbors
+    results["tneighbors"] = tneighbors
+    results["stneighbors"] = stneighbors
+
+    if permutations > 0:
+        exceedence = 0
+        n = len(sneighbors)
+        ids = np.arange(n)
+        if keep:
+            ST = np.zeros(permutations)
+
+        for perm in range(permutations):
+            st = 0
+            rids = np.random.permutation(ids)
+            for i in range(n):
+                ri = rids[i]
+                tni = tneighbors[ri]
+                rjs = [rids[j] for j in sneighbors[i]]
+                sti = [j for j in rjs if j in tni]
+                st += len(sti)
+            st /= 2
+            if st >= results["nst"]:
+                exceedence += 1
+            if keep:
+                ST[perm] = st
+        results["p_value_sim"] = (exceedence + 1) / (permutations + 1)
+        results["exceedence"] = exceedence
+        if keep:
+            results["st_perm"] = ST
+
+    return results
+
+
+class Knox:
+    """
+    Global Knox statistic for space-time interactions
+
+    Parameters
+    ----------
+
+    s_coords: array-like
+      spatial coordinates of point events
+
+    t_coords: array-like
+      temporal coordinates of point events (floats or ints, not dateTime)
+
+    delta: float
+      spatial threshold defining distance below which pairs are spatial
+      neighbors
+
+    tau: float
+      temporal threshold defining distance below which pairs are temporal
+      neighbors
+
+    permutations: int
+      number of random permutations for inference
+
+    keep: bool
+      whether to store realized values of the statistic under permutations
+
+
+
+    Attributes
+    ----------
+
+    s_coords: array-like
+      spatial coordinates of point events
+
+    t_coords: array-like
+      temporal coordinates of point events (floats or ints, not dateTime)
+
+    delta: float
+      spatial threshold defining distance below which pairs are spatial
+      neighbors
+
+    tau: float
+      temporal threshold defining distance below which pairs are temporal
+      neighbors
+
+    permutations: int
+      number of random permutations for inference
+
+    keep: bool
+      whether to store realized values of the statistic under permutations
+
+    nst: int
+      number of space-time pairs
+
+    p_poisson: float
+      Analytical p-value under Poisson assumption
+
+    p_sim: float
+      Pseudo p-value based on random permutations
+
+    expected: array
+      Two-by-two array with expected counts under the null of no space-time
+      interactions. [[NST, NS_], [NT_, N__]] where NST is the expected number
+      of space-time pairs, NS_ is the expected number of spatial (but not also
+      temporal) pairs, NT_ is the number of expected temporal (but not also
+      spatial pairs), N__ is the number of pairs that are neighor spatial or
+      temporal neighbors.
+
+    observed: array
+      Same structure as expected with the observed pair classifications
+
+    sim: array
+      Global statistics from permutations (if keep=True)
+
+
+    Notes
+    -----
+
+    Technical details can be found in :cite:`Rogerson:2001`
+
+
+    Examples
+    --------
+    >>> import libpysal
+    >>> path = libpysal.examples.get_path('burkitt.shp')
+    >>> import geopandas
+    >>> df = geopandas.read_file(path)
+    >>> from pointpats.spacetime import Knox
+    >>> global_knox = Knox(df[['X', 'Y']], df[["T"]], delta=20, tau=5)
+    >>> global_knox.statistic_
+    13
+    >>> global_knox.p_poisson
+    0.14624558197140414
+    >>> global_knox.observed
+    array([[1.300e+01, 3.416e+03],
+           [3.900e+01, 1.411e+04]])
+    >>> global_knox.expected
+    array([[1.01438161e+01, 3.41885618e+03],
+           [4.18561839e+01, 1.41071438e+04]])
+    >>> hasattr(global_knox, 'sim')
+    False
+    >>> import numpy
+    >>> numpy.random.seed(12345)
+    >>> global_knox = Knox(df[['X', 'Y']], df[["T"]], delta=20, tau=5, keep=True)
+    >>> hasattr(global_knox, 'sim')
+    True
+    >>> global_knox.p_sim
+    0.21
+    """
+
+    def __init__(self, s_coords, t_coords, delta, tau, permutations=99, keep=False):
+        self.s_coords = s_coords
+        self.t_coords = t_coords
+        self.delta = delta
+        self.tau = tau
+        self.permutations = permutations
+        self.keep = keep
+        results = _knox(s_coords, t_coords, delta, tau, permutations, keep)
+        self.nst = int(results["nst"])
+        if permutations > 0:
+            self.p_sim = results["p_value_sim"]
+            if keep:
+                self.sim = results["st_perm"]
+
+        self.p_poisson = results["p_value_poisson"]
+        self.observed = results["observed"]
+        self.expected = results["expected"]
+        self.statistic_ = self.nst
+
+    @classmethod
+    def from_dataframe(
+        cls,
+        dataframe,
+        time_col: int,
+        delta: int,
+        tau: int,
+        permutations: int = 99,
+        keep: bool = False,
+    ):
+        """Compute a Knox statistic from a dataframe of Point observations
+
+        Parameters
+        ----------
+        dataframe : geopandas.GeoDataFrame
+            geodataframe holding observations. Should be in a projected coordinate
+            system with geometries stored as Point
+        time_col : str
+            column in the dataframe storing the time values (integer coordinate)
+            for each observation. For example if the observations are stored with
+            a timestamp, the time_col should be converted to a series of integers
+            representing, e.g. hours, days, seconds, etc.
+        delta : int
+            delta parameter defining the spatial neighbor threshold measured in the
+            same units as the dataframe CRS
+        tau : int
+            tau parameter defining the temporal neihgbor threshold (in the units
+            measured by `time_col`)
+        permutations : int, optional
+            permutations to use for computation inference, by default 99
+        keep : bool
+            whether to store realized values of the statistic under permutations
+
+        Returns
+        -------
+        pointpats.spacetime.Knox
+            a fitted Knox class
+
+        """
+        s_coords, t_coords = _spacetime_points_to_arrays(dataframe, time_col)
+
+        return cls(s_coords, dataframe[[time_col]], delta, tau, permutations, keep)
+
+
+def _knox_local(s_coords, t_coords, delta, tau, permutations=99, keep=False, crit=0.05):
+    """
+
+    Parameters
+    ----------
+
+    s_coords: array (nx2)
+        spatial coordinates
+
+    t_coords: array (nx1)
+        temporal coordinates
+
+    delta: numeric
+        spatial threshold distance for neighbor relation
+
+    tau: numeric
+        temporal threshold distance for neighbor relation
+
+    permutations: int
+        number of permutations for conditional randomization inference
+
+    keep: bool
+        whether to store local statistics from the permtuations
+
+    crit: float
+        signficance level for determination of hot spots
+
+    """
+    # think about passing in the global object as an option to avoid recomputing the trees
+    res = _knox(s_coords, t_coords, delta, tau, permutations=permutations)
+    sneighbors = {i: tuple(ns) for i, ns in enumerate(res["sneighbors"])}
+    tneighbors = {i: tuple(nt) for i, nt in enumerate(res["tneighbors"])}
+
+    n = len(s_coords)
+    ids = np.arange(n)
+    res["nsti"] = np.zeros(n)  # number of observed st_pairs for observation i
+    res["nsi"] = [len(r) for r in res["sneighbors"]]
+    res["nti"] = [len(r) for r in res["tneighbors"]]
+    for pair in res["st_pairs"]:
+        i, j = pair
+        res["nsti"][i] += 1
+        res["nsti"][j] += 1
+
+    nsti = res["nsti"]
+    nsi = res["nsi"]
+    nti = res["nti"]
+
+    # rather than do n*permutations, we reuse the permutations
+    # ensuring that each permutation is conditional on a focal unit i
+    # for each of the permutations we loop over i and swap labels between the
+    # label at index i in the current permutation and the label at the index
+    # assigned i in the permutation.
+
+    if permutations > 0:
+        exceedence = np.zeros(n)
+        if keep:
+            STI = np.zeros((n, permutations))
+        for perm in range(permutations):
+            rids = np.random.permutation(ids)
+            for i in range(n):
+                rids_i = rids.copy()
+                # set observed value of focal unit i
+                # swap with value assigned to rids[i]
+                # example
+                # 0 1 2 (ids)
+                # 2 0 1 (rids)
+                # i=0
+                # 0 2 1 (rids_i)
+                # i=1
+                # 2 1 0 (rids_i)
+                # i=2
+                # 1 0 2 (rids_i)
+
+                rids_i[rids == i] = rids[i]
+                rids_i[i] = i
+
+                # calculate local stat
+                rjs = [rids_i[j] for j in sneighbors[i]]
+                tni = tneighbors[i]
+                sti = [j for j in rjs if j in tni]
+                count = len(sti)
+                if count >= res["nsti"][i]:
+                    exceedence[i] += 1
+                if keep:
+                    STI[i, perm] = count
+
+        if keep:
+            res["sti_perm"] = STI
+        res["exceedence_pvalue"] = (exceedence + 1) / (permutations + 1)
+        res["exceedences"] = exceedence
+
+    # analytical inference
+    ntjis = [len(r) for r in res["tneighbors"]]
+    n1 = n - 1
+    hg_pvalues = [
+        1 - hypergeom.cdf(nsti[i] - 1, n1, ntjis, nsi[i]).mean() for i in range(n)
+    ]
+    res["hg_pvalues"] = np.array(hg_pvalues)
+
+    # identification of hot spots
+
+    adjlist = []
+    for i, j in res["st_pairs"]:
+        adjlist.append([i, j])
+    adjlist = pandas.DataFrame(data=adjlist, columns=["focal", "neighbor"])
+    adjlist = adjlist.sort_values(by=["focal", "neighbor"])
+    adjlist.reset_index(drop=True, inplace=True)
+
+    adjlist["orientation"] = ""
+    for index, row in adjlist.iterrows():
+        focal = row["focal"]
+        neighbor = row["neighbor"]
+        ft = t_coords[focal]
+        nt = t_coords[neighbor]
+        if ft < nt:
+            adjlist.iloc[index, 2] = "lead"
+        elif ft > nt:
+            adjlist.iloc[index, 2] = "lag"
+        else:
+            adjlist.iloc[index, 2] = "coincident"
+
+    res["stadjlist"] = adjlist
+    return res
+
+
+class KnoxLocal:
+    """
+    Local Knox statistics for space-time interactions
+
+    Parameters
+    ----------
+
+    s_coords: array (nx2)
+      spatial coordinates of point events
+
+    t_coords: array (nx1)
+      temporal coordinates of point events (floats or ints, not dateTime)
+
+    delta: float
+      spatial threshold defining distance below which pairs are spatial
+      neighbors
+
+    tau: float
+      temporal threshold defining distance below which pairs are temporal
+      neighbors
+
+    permutations: int
+      number of random permutations for inference
+
+    keep: bool
+      whether to store realized values of the statistic under permutations
+
+    conditional: bool
+      whether to include conditional permutation inference
+
+    crit: float
+      signifcance level for local statistics
+
+    crs: str (optional)
+      coordinate reference system string for s_coords
+
+
+
+    Attributes
+    ----------
+
+    s_coords: array (nx2)
+      spatial coordinates of point events
+
+    t_coords: array (nx1)
+      temporal coordinates of point events (floats or ints, not dateTime)
+
+    delta: float
+      spatial threshold defining distance below which pairs are spatial
+      neighbors
+
+    tau: float
+      temporal threshold defining distance below which pairs are temporal
+      neighbors
+
+    permutations: int
+      number of random permutations for inference
+
+    keep: bool
+      whether to store realized values of the statistic under permutations
+
+    nst: int
+      number of space-time pairs (global)
+
+    p_poisson: float
+      Analytical p-value under Poisson assumption (global)
+
+    p_sim: float
+      Pseudo p-value based on random permutations (global)
+
+    expected: array
+      Two-by-two array with expected counts under the null of no space-time
+      interactions. [[NST, NS_], [NT_, N__]] where NST is the expected number
+      of space-time pairs, NS_ is the expected number of spatial (but not also
+      temporal) pairs, NT_ is the number of expected temporal (but not also
+      spatial pairs), N__ is the number of pairs that are neighor spatial or
+      temporal neighbors. (global)
+
+    observed: array
+      Same structure as expected with the observed pair classifications (global)
+
+    sim: array
+      Global statistics from permutations (if keep=True and keep=True) (global)
+
+    p_sims: array
+      Local psuedo p-values from conditional permutations (if permutations>0)
+
+    sims: array
+      Local statistics from conditional permutations (if keep=True and
+      permutations>0)
+
+    nsti: array
+      Local statistics
+
+    p_hypergeom: array
+      Analytical p-values based on hypergeometric distribution
+
+
+
+    Notes
+    -----
+
+    Technical details can be found in :cite:`Rogerson:2001`. The conditional
+    permutation inference is unique to pysal.pointpats.
+
+
+    Examples
+    -------
+    >>> import libpysal
+    >>> path = libpysal.examples.get_path('burkitt.shp')
+    >>> import geopandas
+    >>> df = geopandas.read_file(path)
+    >>> from pointpats.spacetime import Knox
+    >>> import numpy
+    >>> numpy.random.seed(12345)
+    >>> local_knox = KnoxLocal(df[['X', 'Y']], df[["T"]], delta=20, tau=5, keep=True)
+    >>> local_knox.statistic_.shape
+    (188,)
+    >>> lres = local_knox
+    >>> gt0ids = numpy.where(lres.nsti>0)
+    >>> gt0ids # doctest: +NORMALIZE_WHITESPACE
+    (array([ 25,  26,  30,  31,  35,  36,  41,  42,  46,  47,  51,  52, 102,
+              103, 116, 118, 122, 123, 137, 138, 139, 140, 158, 159, 162, 163]),)
+    >>> lres.nsti[gt0ids]
+    array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
+           1., 1., 1., 1., 1., 1., 1., 1., 1.])
+    >>> lres.p_hypergeom[gt0ids]
+    array([0.1348993 , 0.14220663, 0.07335085, 0.08400282, 0.1494317 ,
+           0.21524073, 0.0175806 , 0.04599869, 0.17523687, 0.18209188,
+           0.19111321, 0.16830444, 0.13734428, 0.14703242, 0.06796364,
+           0.03192559, 0.13734428, 0.17523687, 0.12998154, 0.1933476 ,
+           0.13244507, 0.13244507, 0.12502644, 0.14703242, 0.12502644,
+           0.12998154])
+    >>> lres.p_sims[gt0ids]
+    array([0.3 , 0.33, 0.11, 0.17, 0.3 , 0.42, 0.06, 0.06, 0.33, 0.34, 0.36,
+           0.38, 0.3 , 0.29, 0.41, 0.19, 0.31, 0.39, 0.18, 0.39, 0.48, 0.41,
+           0.22, 0.41, 0.39, 0.32])
+    """
+
+    def __init__(
+        self,
+        s_coords,
+        t_coords,
+        delta,
+        tau,
+        permutations=99,
+        keep=False,
+        crit=0.05,
+        crs=None,
+    ):
+
+        if not isinstance(t_coords, np.ndarray):
+            raise ValueError("t_coords  should be numpy.ndarray type")
+        if not isinstance(s_coords, np.ndarray):
+            raise ValueError("s_coords  should be numpy.ndarray type")
+        n_s, k = s_coords.shape
+        if k < 2:
+            raise ValueError("s_coords shape required to be nx2")
+        n_t, k = t_coords.shape
+        if n_s != n_t:
+            raise ValueError("t_coords and s_coords need to be same length")
+
+        self.s_coords = s_coords
+        self.t_coords = t_coords
+        self.delta = delta
+        self.tau = tau
+        self.permutations = permutations
+        self.keep = keep
+        self.crit = crit
+        results = _knox_local(s_coords, t_coords, delta, tau, permutations, keep, crit)
+        self.adjlist = results["stadjlist"]
+        self.nst = int(results["nst"])
+        if permutations > 0:
+            self.p_sim = results["p_value_sim"]
+            if keep:
+                self.sim = results["sti_perm"]
+
+        self.p_poisson = results["p_value_poisson"]
+        self.observed = results["observed"]
+        self.expected = results["expected"]
+        self.p_hypergeom = results["hg_pvalues"]
+        if permutations > 0:
+            self.p_sims = results["exceedence_pvalue"]
+            if keep:
+                self.sims = results["sti_perm"]
+        self.nsti = results["nsti"]
+        # self.hot_spots = results["hot_spots"]
+        self._crs = crs
+        self.statistic_ = self.nsti
+        # reconstruct df
+        geom = gpd.points_from_xy(self.s_coords[:, 0], self.s_coords[:, 1])
+        _gdf = gpd.GeoDataFrame(geometry=geom, crs=self._crs)
+        _gdf["t_coords"] = self.t_coords
+        self._gdf = _gdf.reset_index()
+
+    @classmethod
+    def from_dataframe(
+        cls,
+        dataframe,
+        time_col: str,
+        delta: int,
+        tau: int,
+        permutations: int = 99,
+        keep: bool = False,
+        crit: float = 0.05,
+    ):
+        """Compute a set of local Knox statistics from a dataframe of Point observations
+
+        Parameters
+        ----------
+        dataframe : geopandas.GeoDataFrame
+            dataframe holding observations. Should be in a projected coordinate system
+            with geometries stored as Points
+        time_col : str
+            column in the dataframe storing the time values (integer coordinate)
+            for each observation. For example if the observations are stored with
+            a timestamp, the time_col should be converted to a series of integers
+            representing, e.g. hours, days, seconds, etc.
+        delta : int
+            delta parameter defining the spatial neighbor threshold measured in the
+            same units as the dataframe CRS
+        tau : int
+            tau parameter defining the temporal neihgbor threshold (in the units
+            measured by `time_col`)
+        permutations : int, optional
+            permutations to use for computational inference, by default 99
+        keep : bool
+            whether to store realized values of the statistic under permutations
+        crit: float
+            signficance level for determination of hot spots
+
+        Returns
+        -------
+        pointpats.spacetime.LocalKnox
+            a fitted KnoxLocal class
+
+        """
+        s_coords, t_coords = _spacetime_points_to_arrays(dataframe, time_col)
+
+        return cls(
+            s_coords, t_coords, delta, tau, permutations, keep, crs=dataframe.crs
+        )
+
+    def hot_spots(self, crit=0.05, inference="permutation"):
+        if inference == "permutation":
+            if not hasattr(self, "p_sims"):
+                warn(
+                    "Pseudo-p values not availalable. Permutation-based p-values require "
+                    "fitting the KnoxLocal class using `permutations` set to a large "
+                    "number. Using analytic p-values instead"
+                )
+                ps = self.p_hypergeom
+            else:
+                ps = self.p_sims
+        elif inference == "analytic":
+            ps = self.p_hypergeom
+        else:
+            raise ValueError("inference must be either `permutation` or `analytic`")
+        # determine hot spots
+        pdf = pandas.DataFrame(data=ps, columns=["pvalue"])
+        pdf_sig = pdf[pdf.pvalue <= crit]
+        adjlist = self.adjlist
+        hot_spots = pandas.merge(
+            adjlist, pdf_sig, how="right", left_on="focal", right_index=True
+        ).reset_index(drop=True)
+
+        return hot_spots
+
+    def plot(
+        self,
+        kind="all",
+        colors={"focal": "red", "neighbor": "blue", "nonsig": "grey"},
+        crit=0.05,
+        inference="permutation",
+    ):
+
+        # logic for conditional formatting (focal as different color than lead/lag neighbors,
+
+        # arrows, close clique as a hull or not
+
+        if kind.lower() == "all":
+            self._gdf["color"] = "grey"
+            self._gdf["pvalue"] = self.p_hypergeom
+            # neighbors = self.adjlist.neighbor.unique()
+            # print(neighbors)
+
+            mask = self._gdf[self._gdf.pvalue <= crit].index.values
+            neighbors = self.adjlist[self.adjlist.focal.isin(mask)].neighbor.unique()
+            self._gdf.loc[neighbors, "color"] = "blue"
+            self._gdf.loc[self._gdf.pvalue <= crit, "color"] = "red"
+
+            g = self._gdf
+            m = g[g.color == "grey"].plot(color="grey")
+            g[g.color == "blue"].plot(ax=m, color="blue")
+            g[g.color == "red"].plot(ax=m, color="red")
+
+            return m
+
+            # return self._gdf.plot(color=self._gdf.color.values)
+
+        elif kind.lower() == "hotspots":
+            hot_spots = self.hot_spots(crit, inference=inference)
+            return self._gdfhs.plot()
+
+    def explore(
+        self,
+        crit=0.05,
+        inference="permutation",
+        style_kwds=None,
+        tiles="CartoDB Positron",
+    ):
+
+        # logic for conditional formatting (focal as different color than lead/lag neighbors,
+        # arrows, close clique as a hull or not
+
+        # interactivity: on-click, mouse-over functions sending to folium
+
+        # how to populate tool-tip columns
+
+        # markerclustering?
+        if style_kwds is None:
+            style_kwds = {}
+        g = self._gdf.copy()
+
+        g["color"] = "grey"
+
+        if inference == "permutation":
+            if not hasattr(self, "p_sims"):
+                warn(
+                    "Pseudo-p values not availalable. Permutation-based p-values require "
+                    "fitting the KnoxLocal class using `permutations` set to a large "
+                    "number. Using analytic p-values instead"
+                )
+                g["pvalue"] = self.p_hypergeom
+            else:
+                g["pvalue"] = self.p_sims
+        elif inference == "analytic":
+            g["pvalue"] = self.p_hypergeom
+        else:
+            raise ValueError("inference must be either `permutation` or `analytic`")
+
+        mask = g[g.pvalue <= crit].index.values
+        neighbors = self.adjlist[self.adjlist.focal.isin(mask)].neighbor.unique()
+        g.loc[neighbors, "color"] = "blue"
+        g.loc[g.pvalue <= crit, "color"] = "red"
+        nbs = self.adjlist.groupby("focal").agg(list)["neighbor"]
+        g = g.merge(nbs, left_on="index", right_index=True, how="left")
+
+        m = g[g.color == "grey"].explore(
+            color="grey", style_kwds=style_kwds, tiles=tiles
+        )
+        blues = g[g.color == "blue"]
+        if blues.shape[0] == 0:
+            warn("empty neighbor set.")
+        else:
+            m = blues.explore(m=m, color="blue", style_kwds=style_kwds)
+        m = g[g.color == "red"].explore(m=m, color="red", style_kwds=style_kwds)
+
+        # edges between hotspot and st-neighbors
+        ghs = self.hot_spots(crit=crit)
+        print(ghs)
+        ghs = ghs.dropna()
+        print(ghs)
+        origins = g.iloc[ghs.focal].geometry
+        destinations = g.iloc[ghs.neighbor].geometry
+        ods = zip(origins, destinations)
+        lines = gpd.GeoSeries([LineString(od) for od in ods])
+        lines.crs = g.crs
+        lines.explore(m=m, color="green")
+
+        return m
+
+    def _gdfhs(self):
+        # merge df with self.hotspots
+        return self._gdf.merge(self.hot_spots, left_index=True, right_on="focal")
+
+
+def _spacetime_points_to_arrays(dataframe, time_col):
+    """convert long-form geodataframe into arrays for kdtree
+
+    Parameters
+    ----------
+    dataframe : geopandas.GeoDataFrame
+        geodataframe with point geometries
+    time_col : str
+        name of the column on dataframe that stores time values
+
+    Returns
+    -------
+    tuple
+        two numpy arrays holding spatial coodinates s_coords (n,2)
+        and temporal coordinates t_coords (n,1)
+
+    """
+    if dataframe.crs is None:
+        warn(
+            "There is no CRS set on the dataframe. The KDTree will assume coordinates "
+            "are stored in Euclidean distances"
+        )
+    else:
+        if dataframe.crs.is_geographic:
+            raise ValueError(
+                "The input dataframe must be in a projected coordinate system."
+            )
+
+    assert dataframe.geom_type.unique().tolist() == [
+        "Point"
+    ], "The Knox statistic is only defined for Point geometries"
+
+    # kdtree wont operate on datetime
+    if is_numeric_dtype(dataframe[time_col].dtype) is False:
+        raise ValueError(
+            "The time values must be stored as "
+            f"a numeric dtype but the column {time_col} is stored as "
+            f"{dataframe[time_col].dtype}"
+        )
+
+    s_coords = np.vstack((dataframe.geometry.x.values, dataframe.geometry.y.values)).T
+    t_coords = np.vstack(dataframe[time_col].values)
+
+    return s_coords, t_coords
+
+
+def _time_to_int(df, column, fmt="%Y%m%d"):
+    """Create an integer valued column for time"""
+    time_series = pandas.to_datetime(df[column], format=fmt)
+    min_time = time_series.min()
+    time_int = time_series.appy(lambda x: x - min_time)
+    return time_int.dt.days


Test patch:
diff --git a/pointpats/tests/test_spacetime.py b/pointpats/tests/test_spacetime.py
index 94d1ffb..38a939d 100644
--- a/pointpats/tests/test_spacetime.py
+++ b/pointpats/tests/test_spacetime.py
@@ -1,35 +1,336 @@
-import unittest
+from warnings import warn
+
+import geopandas as gpd
 import libpysal as lps
-from pointpats import SpaceTimeEvents, knox, mantel, jacquez, modified_knox
-import scipy
+import numpy
+from pytest import approx
+
+from pointpats import (
+    Knox,
+    KnoxLocal,
+    SpaceTimeEvents,
+    jacquez,
+    knox,
+    mantel,
+    modified_knox,
+)
+
+
+class TestKnox:
+    def setup_method(self):
+        path = lps.examples.get_path("burkitt.shp")
+        self.gdf = gpd.read_file(path)
+
+    def test_knox(self):
+        global_knox = Knox(self.gdf[["X", "Y"]], self.gdf[["T"]], delta=20, tau=5)
+        assert global_knox.statistic_ == 13
+        assert global_knox.p_poisson == 0.14624558197140414
+        assert hasattr(global_knox, "sim") == False
+        numpy.testing.assert_array_equal(
+            global_knox.observed, [[1.300e01, 3.416e03], [3.900e01, 1.411e04]]
+        )
+        numpy.testing.assert_allclose(
+            global_knox.expected,
+            [[1.01438161e01, 3.41885618e03], [4.1856139e01, 1.41071438e04]],
+            rtol=1e-5,
+            atol=0,
+        )
+
+        numpy.random.seed(12345)
+        global_knox = Knox(
+            self.gdf[["X", "Y"]], self.gdf[["T"]], delta=20, tau=5, keep=True
+        )
+        assert global_knox.statistic_ == 13
+        assert hasattr(global_knox, "sim") == True
+        assert global_knox.p_sim == 0.21
 
+    def test_knox_from_gdf(self):
+        gdf = self.gdf.copy()
+        # not technically the correct CRS...
+        gdf.crs = 21096
+        global_knox = Knox.from_dataframe(gdf, time_col="T", delta=20, tau=5)
+        assert global_knox.statistic_ == 13
+        assert global_knox.p_poisson == 0.14624558197140414
+        assert hasattr(global_knox, "sim") == False
+        numpy.testing.assert_array_equal(
+            global_knox.observed, [[1.300e01, 3.416e03], [3.900e01, 1.411e04]]
+        )
+        numpy.testing.assert_allclose(
+            global_knox.expected,
+            [[1.01438161e01, 3.41885618e03], [4.1856139e01, 1.41071438e04]],
+            rtol=1e-5,
+            atol=0,
+        )
+
+        # no CRS should raise a warning
+        global_knox = Knox.from_dataframe(self.gdf, time_col="T", delta=20, tau=5)
+        numpy.testing.assert_allclose(
+            global_knox.expected,
+            [[1.01438161e01, 3.41885618e03], [4.1856139e01, 1.41071438e04]],
+            rtol=1e-5,
+            atol=0,
+        )
+
+        # unprojected coords
+        try:
+            gdf.crs = 4326
+            global_knox = Knox.from_dataframe(gdf, time_col="T", delta=20, tau=5)
+        except ValueError:
+            warn("successfully caught crs error")
+            pass
 
-class SpaceTimeEvents_Tester(unittest.TestCase):
-    def setUp(self):
-        self.path = lps.examples.get_path("burkitt.shp")
+        # non-numeric type for time
+        try:
+            gdf["T"] = gdf["T"].astype("O")
+            global_knox = Knox.from_dataframe(gdf, time_col="T", delta=20, tau=5)
+        except ValueError:
+            warn("successfully caught dtype error")
+            pass
 
-    def test_SpaceTimeEvents(self):
-        events = SpaceTimeEvents(self.path, "T")
-        self.assertEqual(events.n, 188)
-        self.assertEqual(list(events.space[0]), [300.0, 302.0])
-        self.assertEqual(list(events.t[0]), [413])
+        numpy.random.seed(12345)
+        global_knox = Knox(
+            self.gdf[["X", "Y"]], self.gdf[["T"]], delta=20, tau=5, keep=True
+        )
+        assert global_knox.statistic_ == 13
+        assert hasattr(global_knox, "sim") == True
+        assert global_knox.p_sim == 0.21
 
 
-class Knox_Tester(unittest.TestCase):
-    def setUp(self):
+class TestKnoxLocal:
+    def setup_method(self):
         path = lps.examples.get_path("burkitt.shp")
-        self.events = SpaceTimeEvents(path, "T")
+        self.gdf = gpd.read_file(path)
 
-    def test_knox(self):
-        result = knox(self.events.space, self.events.t, delta=20, tau=5, permutations=1)
-        self.assertEqual(result["stat"], 13.0)
+    def test_knox_local(self):
+        numpy.random.seed(12345)
+        local_knox = KnoxLocal(
+            self.gdf[["X", "Y"]].values, self.gdf[["T"]].values, delta=20, tau=5, keep=True
+        )
+        assert local_knox.statistic_.shape == (188,)
+        lres = local_knox
+        gt0ids = numpy.where(lres.nsti > 0)
+        numpy.testing.assert_array_equal(
+            gt0ids,
+            [
+                [
+                    25,
+                    26,
+                    30,
+                    31,
+                    35,
+                    36,
+                    41,
+                    42,
+                    46,
+                    47,
+                    51,
+                    52,
+                    102,
+                    103,
+                    116,
+                    118,
+                    122,
+                    123,
+                    137,
+                    138,
+                    139,
+                    140,
+                    158,
+                    159,
+                    162,
+                    163,
+                ]
+            ],
+        )
+        numpy.testing.assert_allclose(
+            lres.p_hypergeom[gt0ids],
+            [
+                0.1348993,
+                0.14220663,
+                0.07335085,
+                0.08400282,
+                0.1494317,
+                0.21524073,
+                0.0175806,
+                0.04599869,
+                0.17523687,
+                0.18209188,
+                0.19111321,
+                0.16830444,
+                0.13734428,
+                0.14703242,
+                0.06796364,
+                0.03192559,
+                0.13734428,
+                0.17523687,
+                0.12998154,
+                0.1933476,
+                0.13244507,
+                0.13244507,
+                0.12502644,
+                0.14703242,
+                0.12502644,
+                0.12998154,
+            ],
+            rtol=1e-5,
+            atol=0,
+        )
+        numpy.testing.assert_array_equal(
+            lres.p_sims[gt0ids],
+            [
+                0.3,
+                0.33,
+                0.11,
+                0.17,
+                0.3,
+                0.42,
+                0.06,
+                0.06,
+                0.33,
+                0.34,
+                0.36,
+                0.38,
+                0.3,
+                0.29,
+                0.41,
+                0.19,
+                0.31,
+                0.39,
+                0.18,
+                0.39,
+                0.48,
+                0.41,
+                0.22,
+                0.41,
+                0.39,
+                0.32,
+            ],
+        )
 
+    def test_knox_local_from_gdf(self):
+        gdf = self.gdf
+        gdf.crs = 21096
+        numpy.random.seed(12345)
+        local_knox = KnoxLocal.from_dataframe(
+            gdf, time_col="T", delta=20, tau=5, keep=True
+        )
+        assert local_knox.statistic_.shape == (188,)
+        lres = local_knox
+        gt0ids = numpy.where(lres.nsti > 0)
+        numpy.testing.assert_array_equal(
+            gt0ids,
+            [
+                [
+                    25,
+                    26,
+                    30,
+                    31,
+                    35,
+                    36,
+                    41,
+                    42,
+                    46,
+                    47,
+                    51,
+                    52,
+                    102,
+                    103,
+                    116,
+                    118,
+                    122,
+                    123,
+                    137,
+                    138,
+                    139,
+                    140,
+                    158,
+                    159,
+                    162,
+                    163,
+                ]
+            ],
+        )
+        numpy.testing.assert_allclose(
+            lres.p_hypergeom[gt0ids],
+            [
+                0.1348993,
+                0.14220663,
+                0.07335085,
+                0.08400282,
+                0.1494317,
+                0.21524073,
+                0.0175806,
+                0.04599869,
+                0.17523687,
+                0.18209188,
+                0.19111321,
+                0.16830444,
+                0.13734428,
+                0.14703242,
+                0.06796364,
+                0.03192559,
+                0.13734428,
+                0.17523687,
+                0.12998154,
+                0.1933476,
+                0.13244507,
+                0.13244507,
+                0.12502644,
+                0.14703242,
+                0.12502644,
+                0.12998154,
+            ],
+            rtol=1e-5,
+            atol=0,
+        )
+        numpy.testing.assert_array_equal(
+            lres.p_sims[gt0ids],
+            [
+                0.3,
+                0.33,
+                0.11,
+                0.17,
+                0.3,
+                0.42,
+                0.06,
+                0.06,
+                0.33,
+                0.34,
+                0.36,
+                0.38,
+                0.3,
+                0.29,
+                0.41,
+                0.19,
+                0.31,
+                0.39,
+                0.18,
+                0.39,
+                0.48,
+                0.41,
+                0.22,
+                0.41,
+                0.39,
+                0.32,
+            ],
+        )
 
-class Mantel_Tester(unittest.TestCase):
-    def setUp(self):
+
+# old tests refactored to pytest
+
+
+class TestSpaceTimeEvents:
+    def setup_method(self):
         path = lps.examples.get_path("burkitt.shp")
         self.events = SpaceTimeEvents(path, "T")
 
+    def test_space_time_events(self):
+        assert self.events.n == 188
+
+    def test_knox(self):
+        result = knox(self.events.space, self.events.t, delta=20, tau=5, permutations=1)
+        assert result["stat"] == 13.0
+
     def test_mantel(self):
         result = mantel(
             self.events.space,
@@ -40,43 +341,17 @@ def test_mantel(self):
             tcon=0.0,
             tpow=1.0,
         )
-        self.assertAlmostEqual(result["stat"], 0.014154, 6)
-
 
-class Jacquez_Tester(unittest.TestCase):
-    def setUp(self):
-        path = lps.examples.get_path("burkitt.shp")
-        self.events = SpaceTimeEvents(path, "T")
+        assert result["stat"] == approx(0.014154, rel=1e-4)
 
     def test_jacquez(self):
         result = jacquez(self.events.space, self.events.t, k=3, permutations=1)
-        self.assertEqual(result["stat"], 12)
 
-
-class ModifiedKnox_Tester(unittest.TestCase):
-    def setUp(self):
-        path = lps.examples.get_path("burkitt.shp")
-        self.events = SpaceTimeEvents(path, "T")
+        assert result["stat"] == 12
 
     def test_modified_knox(self):
         result = modified_knox(
             self.events.space, self.events.t, delta=20, tau=5, permutations=1
         )
-        self.assertAlmostEqual(result["stat"], 2.810160, 6)
-
-
-suite = unittest.TestSuite()
-test_classes = [
-    SpaceTimeEvents_Tester,
-    Knox_Tester,
-    Mantel_Tester,
-    Jacquez_Tester,
-    ModifiedKnox_Tester,
-]
-for i in test_classes:
-    a = unittest.TestLoader().loadTestsFromTestCase(i)
-    suite.addTest(a)
 
-if __name__ == "__main__":
-    runner = unittest.TextTestRunner()
-    runner.run(suite)
+        assert result["stat"] == approx(2.810160, rel=1e-4)