{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Demonstrate the following interpolation methods\n",
    "* Bilinear interpolation\n",
    "* Inverse distance weighting\n",
    "* Ordinary kriging\n",
    "* Radial basis function regression\n",
    "\n",
    "Annual mean PM2.5 concentrations are used as an example variable."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Packages that we need\n",
    "import numpy as np\n",
    "import xarray as xr\n",
    "import pandas as pd\n",
    "import geopandas as gpd\n",
    "import matplotlib.pyplot as plt\n",
    "import cartopy.crs as ccrs\n",
    "import cartopy.feature as cfeature\n",
    "import acgc.figstyle"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Read PM2.5 and AQI data\n",
    "point_data = pd.read_csv( 'EPA_AQS_PM25_Florida_2022.csv' )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<cartopy.mpl.feature_artist.FeatureArtist at 0x106d09720>"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 79144 characters]",
      "text/plain": [
       "<Figure size 400x400 with 2 Axes>"
      ]
     },
     "metadata": {
      "image/png": {
       "height": 400,
       "width": 400
      }
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Display the point data on a map\n",
    "projFL = ccrs.TransverseMercator( central_longitude=( point_data['Longitude'].min() + point_data['Longitude'].max() ) / 2,\n",
    "                                  central_latitude=( point_data['Latitude'].min() + point_data['Latitude'].max() ) / 2 )\n",
    "\n",
    "ax = plt.subplot(111, projection=projFL)\n",
    "point_data.plot.scatter( 'Longitude', 'Latitude', c='PM2.5', \n",
    "                  ax=ax,\n",
    "                  transform=ccrs.PlateCarree() )\n",
    "ax.coastlines()\n",
    "ax.add_feature(cfeature.STATES)\n",
    "ax.add_feature(cfeature.LAKES, edgecolor='black', facecolor='none')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Degrees of Latitude and Longitude are not equal distance increments, \n",
    "# Convert to a regional projection using meters in x and y directions\n",
    "result = projFL.transform_points( ccrs.PlateCarree(), \n",
    "                                 point_data['Longitude'], \n",
    "                                 point_data['Latitude'] )\n",
    "point_data['x'] = result[:,0]\n",
    "point_data['y'] = result[:,1]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create a grid with regular spacing in x and y\n",
    "# The point data will be interpolated to this new grid\n",
    "\n",
    "# Extend the grid this distance outside the data points, m\n",
    "buffer = 100e3\n",
    "\n",
    "xnew = np.linspace( point_data['x'].min()-buffer,\n",
    "                    point_data['x'].max()+buffer,\n",
    "                    200 )\n",
    "ynew = np.linspace( point_data['y'].min()-buffer,\n",
    "                    point_data['y'].max()+buffer, \n",
    "                    200 )\n",
    "\n",
    "# Broadcast these together\n",
    "xnew2d, ynew2d = np.meshgrid( xnew, ynew )\n",
    "\n",
    "# Save the new grid in a dataframe with columns\n",
    "new_grid = pd.DataFrame( {'x':xnew2d.ravel(), \n",
    "                          'y':ynew2d.ravel()} )\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Linear Interpolation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [],
   "source": [
    "from scipy.interpolate import griddata\n",
    "\n",
    "# Note that griddata has other interpolation methods available as well\n",
    "predict_bilinear = griddata( point_data[['x','y']], \n",
    "                            point_data['PM2.5'], \n",
    "                            new_grid, \n",
    "                            method='linear' ).\\\n",
    "                    reshape( xnew2d.shape )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Radial basis function"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [],
   "source": [
    "from scipy.interpolate import RBFInterpolator\n",
    "\n",
    "predict_RBF = RBFInterpolator( point_data[['x','y']], \n",
    "                           point_data['PM2.5'],\n",
    "                           kernel='linear' )( new_grid ).\\\n",
    "                    reshape( xnew2d.shape )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Inverse distance weighting\n",
    "\n",
    "In KNeighborsRegressor, specify the number of neighboring points that will be combined.\n",
    "\n",
    "In RadiusNeighborsRegressor, all points within the specifed radius will be combined."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/cdholmes/anaconda3/envs/general/lib/python3.10/site-packages/sklearn/neighbors/_regression.py:500: UserWarning: One or more samples have no neighbors within specified radius; predicting NaN.\n",
      "  warnings.warn(empty_warning_msg)\n"
     ]
    }
   ],
   "source": [
    "# Inverse distance weighting\n",
    "from sklearn.neighbors import KNeighborsRegressor, RadiusNeighborsRegressor\n",
    "\n",
    "model = KNeighborsRegressor( n_neighbors=40,\n",
    "                                weights='distance').fit( point_data[['x','y']],\n",
    "                                                            point_data['PM2.5'] )\n",
    "model = RadiusNeighborsRegressor( radius=100e3, #Distance in meters\n",
    "                                  weights='distance').fit( point_data[['x','y']],\n",
    "                                                            point_data['PM2.5'] )\n",
    "predict_IDW = model.predict( new_grid ).\\\n",
    "                    reshape( xnew2d.shape )\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Ordinary Kriging\n",
    "\n",
    "Kriging is a geostatistical method and a special case of Gaussian Process Regression. Unlike the other methods here, kriging provides uncertainty estimate along with values. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 80,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/cdholmes/anaconda3/envs/general/lib/python3.10/site-packages/sklearn/gaussian_process/kernels.py:419: ConvergenceWarning: The optimal value found for dimension 0 of parameter k1__k1__constant_value is close to the specified lower bound 1e-08. Decreasing the bound and calling fit again may find a better value.\n",
      "  warnings.warn(\n",
      "/Users/cdholmes/anaconda3/envs/general/lib/python3.10/site-packages/sklearn/gaussian_process/kernels.py:419: ConvergenceWarning: The optimal value found for dimension 0 of parameter k1__k2__length_scale is close to the specified lower bound 5000.0. Decreasing the bound and calling fit again may find a better value.\n",
      "  warnings.warn(\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "0.0001**2 + RBF(length_scale=5e+03) + WhiteKernel(noise_level=0.186)"
      ]
     },
     "execution_count": 80,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.gaussian_process import GaussianProcessRegressor\n",
    "from sklearn.gaussian_process.kernels import RBF, WhiteKernel, ConstantKernel\n",
    "\n",
    "# Covariance kernel\n",
    "# Units for ConstantKernel and WhiteKernel are same as data (ug/m3)\n",
    "# Units for RBF are distance (m)\n",
    "# WhiteKernel specifies that measurement values have some uncertainty, specified as stddev. \n",
    "# In this example, RBF says that length scale for correlation between measurements is 5-500km, with a first guess of 50km\n",
    "# WhiteKernel says error in an idividual measurement (annual average) is .003-0.5 µg/m3, with a first guess of 0.01\n",
    "# ConstantKernel says that points at any distance from each other are expected to have additional covariance\n",
    "kernel = ConstantKernel(1,(1e-8,100)) + RBF(50e3, (5e3,5e5)) + WhiteKernel(0.01, (1e-3,0.5))\n",
    "# kernel = RBF(50e3, (5e3,5e5)) + WhiteKernel(0.01, (1e-3,0.5))\n",
    "\n",
    "gp = GaussianProcessRegressor(kernel=kernel,\n",
    "                              normalize_y=True,\n",
    "                              n_restarts_optimizer=20)\n",
    "gp.fit( point_data[['x','y']],\n",
    "        point_data['PM2.5'] )\n",
    "\n",
    "predict_krig, predict_krig_std = gp.predict( new_grid,\n",
    "                                            return_std=True )\n",
    "\n",
    "# Reshape the results\n",
    "predict_krig = predict_krig.\\\n",
    "                    reshape( xnew2d.shape )\n",
    "predict_krig_std = predict_krig_std.\\\n",
    "                    reshape( xnew2d.shape )\n",
    "\n",
    "# Print the optimized kernel; Check this for reasonableness \n",
    "# The ConstantKernel is extremely small, so could be omitted\n",
    "gp.kernel_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Display results"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 81,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/cdholmes/anaconda3/envs/general/lib/python3.10/site-packages/cartopy/mpl/geoaxes.py:1781: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3.  Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading'].  This will become an error two minor releases later.\n",
      "  result = super().pcolormesh(*args, **kwargs)\n",
      "/Users/cdholmes/anaconda3/envs/general/lib/python3.10/site-packages/cartopy/mpl/geoaxes.py:1781: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3.  Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading'].  This will become an error two minor releases later.\n",
      "  result = super().pcolormesh(*args, **kwargs)\n",
      "/Users/cdholmes/anaconda3/envs/general/lib/python3.10/site-packages/cartopy/mpl/geoaxes.py:1781: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3.  Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading'].  This will become an error two minor releases later.\n",
      "  result = super().pcolormesh(*args, **kwargs)\n",
      "/Users/cdholmes/anaconda3/envs/general/lib/python3.10/site-packages/cartopy/mpl/geoaxes.py:1781: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3.  Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading'].  This will become an error two minor releases later.\n",
      "  result = super().pcolormesh(*args, **kwargs)\n",
      "/Users/cdholmes/anaconda3/envs/general/lib/python3.10/site-packages/cartopy/mpl/geoaxes.py:1781: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3.  Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading'].  This will become an error two minor releases later.\n",
      "  result = super().pcolormesh(*args, **kwargs)\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 508508 characters]",
      "text/plain": [
       "<Figure size 600x700 with 12 Axes>"
      ]
     },
     "metadata": {
      "image/png": {
       "height": 700,
       "width": 600
      }
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Plot all of the interpolation methods\n",
    "fig, axs = plt.subplots( 3, 2, \n",
    "                        figsize=(6,7),\n",
    "                        subplot_kw={'projection':projFL})\n",
    "\n",
    "# Display data points, coastlines on all panels\n",
    "for ax in axs.flat:\n",
    "    \n",
    "    # Skip data points on last panel\n",
    "    if ax is not axs.flat[-1]:\n",
    "        point_data.plot.scatter( 'Longitude', 'Latitude', c='PM2.5',\n",
    "                            edgecolor='black', \n",
    "                            ax=ax,\n",
    "                            transform=ccrs.PlateCarree(),\n",
    "                            zorder=10 )\n",
    "    ax.coastlines()\n",
    "    ax.add_feature(cfeature.STATES)\n",
    "    ax.add_feature(cfeature.LAKES, edgecolor='black', facecolor='none')\n",
    "\n",
    "    ax.set_extent([-87.5,-79.5,24.5,31.2],ccrs.PlateCarree())\n",
    "\n",
    "axs[0,0].set_title('Measurements')\n",
    "\n",
    "axs[0,1].pcolormesh( xnew2d, ynew2d, predict_bilinear, transform=projFL )\n",
    "axs[0,1].set_title('Bilinear interpolation')\n",
    "\n",
    "axs[1,0].pcolormesh( xnew2d, ynew2d, predict_RBF, transform=projFL )\n",
    "axs[1,0].set_title('Radial basis function')\n",
    "\n",
    "axs[1,1].pcolormesh( xnew2d, ynew2d, predict_IDW, transform=projFL )\n",
    "axs[1,1].set_title('Inverse distance weighting')\n",
    "\n",
    "axs[2,0].pcolormesh( xnew2d, ynew2d, predict_krig, transform=projFL )\n",
    "axs[2,0].set_title('Ordinary Kriging')\n",
    "\n",
    "p=axs[2,1].pcolormesh( xnew2d, ynew2d, predict_krig_std, transform=projFL )\n",
    "axs[2,1].set_title('Ordinary Kriging (stddev)')\n",
    "fig.colorbar(p)\n",
    "\n",
    "plt.tight_layout()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "general",
   "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.10.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
