{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/niconoe/pyinaturalist/main?filepath=examples%2FData%2520Visualizations%2520-%2520Seaborn.ipynb)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Seaborn examples\n",
    "Here are some examples of visualizations that can be created using [Seaborn](https://seaborn.pydata.org/)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "from datetime import datetime\n",
    "from dateutil import tz\n",
    "from os.path import exists\n",
    "from pprint import pprint\n",
    "\n",
    "import seaborn as sns\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib as mpl\n",
    "from matplotlib import dates\n",
    "from matplotlib import pyplot as plt\n",
    "\n",
    "from pyinaturalist import get_observations, get_places_autocomplete\n",
    "\n",
    "BASIC_OBS_COLUMNS = [\n",
    "    'id', 'observed_on', 'location', 'uri', 'taxon.id',\n",
    "    'taxon.name', 'taxon.rank', 'taxon.preferred_common_name', 'user.login',\n",
    "]\n",
    "DATASET_FILENAME = 'midwest_monarchs.json'\n",
    "PLOT_COLOR = '#fa7b23'\n",
    "MIDWEST_STATE_IDS = [3, 20, 24, 25, 28, 32, 35, 38]  # place_ids of 8 states in the Midwest US\n",
    "\n",
    "sns.set_theme(style=\"darkgrid\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "def date_to_mpl_day_of_year(dt):\n",
    "    \"\"\"Get a matplotlib-compatible date number, ignoring the year (to represent day of year)\"\"\"\n",
    "    try:\n",
    "        return dates.date2num(dt.replace(year=datetime.now().year))\n",
    "    except ValueError:\n",
    "        return None\n",
    "\n",
    "def date_to_mpl_time(dt):\n",
    "    \"\"\"Get a matplotlib-compatible date number, ignoring the date (to represent time of day)\"\"\"\n",
    "    try:\n",
    "        return date_to_num(dt) % 1\n",
    "    except ValueError:\n",
    "        return None\n",
    "\n",
    "def to_local_tz(dt):\n",
    "    \"\"\"Convert a datetime object to the local time zone\"\"\"\n",
    "    try:\n",
    "        return dt.astimezone(tz.tzlocal())\n",
    "    except (TypeError, ValueError):\n",
    "        return None\n",
    "                \n",
    "def get_xlim():\n",
    "    \"\"\"Get limits of x axis for first and last days of the year\"\"\"\n",
    "    now = datetime.now()\n",
    "    xmin = dates.date2num(datetime(now.year, 1, 1))\n",
    "    xmax = dates.date2num(datetime(now.year, 12, 31))\n",
    "    return xmin, xmax\n",
    "\n",
    "def get_colormap(color):\n",
    "    \"\"\"Make a colormap (gradient) based on the given color; copied from seaborn.axisgrid\"\"\"\n",
    "    color_rgb = mpl.colors.colorConverter.to_rgb(color)\n",
    "    colors = [sns.set_hls_values(color_rgb, l=l) for l in np.linspace(1, 0, 12)]\n",
    "    return sns.blend_palette(colors, as_cmap=True)\n",
    "\n",
    "def pdir(obj, sort_types=False, non_callables=False):\n",
    "    attrs = {attr: type(getattr(obj, attr)).__name__ for attr in dir(obj)}\n",
    "    if sort_types:\n",
    "        attrs = {k: v for k, v in sorted(attrs.items(), key=lambda x: x[1])}\n",
    "    if non_callables:\n",
    "        attrs = {k: v for k, v in attrs.items() if v not in ['function', 'method', 'method-wrapper', 'builtin_function_or_method']}\n",
    "    pprint(attrs, sort_dicts=not sort_types)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Get all observations for a given place and species"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 478,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{24: 'Iowa',\n",
      " 1911: 'Iowa',\n",
      " 2840: 'Iowa',\n",
      " 8680: 'Iowa City',\n",
      " 119385: 'Iowa Wetland Management District',\n",
      " 125537: 'Terry Trueblood Wetland Exploration Trail',\n",
      " 137891: 'Pammel State Park, Winterset, Iowa',\n",
      " 151098: 'Mount Vernon, Iowa walking path',\n",
      " 161392: 'Upper Iowa River Wildlife Management Areas',\n",
      " 172799: 'Ashton Prairie'}\n"
     ]
    }
   ],
   "source": [
    "# Optional: search for a place ID by name\n",
    "response = get_places_autocomplete(q='iowa')\n",
    "pprint({p['id']: p['name'] for p in  response['results']})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Reload from previously loaded results, if available\n",
    "#if exists(DATASET_FILENAME):\n",
    "#    with open(DATASET_FILENAME) as f:\n",
    "#        observations = json.load(f)\n",
    "#else:\n",
    "observations = get_observations(\n",
    "    taxon_name='Danaus plexippus',\n",
    "    photos=True,\n",
    "    geo=True,\n",
    "    geoprivacy='open',\n",
    "    place_id=MIDWEST_STATE_IDS,\n",
    "    page='all',\n",
    ")\n",
    "# Save results for future usage\n",
    "with open(DATASET_FILENAME, 'w') as f:\n",
    "    json.dump(observations, f, indent=4, sort_keys=True, default=str)\n",
    "        \n",
    "print(f'Total observations: {len(observations)}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Data cleanup"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 453,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Flatten nested JSON values\n",
    "df = pd.json_normalize(observations)\n",
    "\n",
    "# Normalize timezones\n",
    "df['observed_on'] = df['observed_on'].dropna().apply(to_local_tz)\n",
    "\n",
    "# Add some extra date/time columns that matplotlib can more easily handle\n",
    "df['observed_time_mp'] = df['observed_on'].apply(date_to_mpl_time)\n",
    "df['observed_on_mp'] = df['observed_on'].apply(date_to_mpl_day_of_year)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 397,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Optional: narrow down to just a few columns of interest\n",
    "#pprint(list(sorted(df.columns)))\n",
    "#df = df[OBS_COLUMNS]\n",
    "\n",
    "# Optional: Hacky way of setting limits by adding outliers\n",
    "# JointGrid + hexbin doesn't make it easy to do this the 'right' way without distorting the plot\n",
    "#df2 = pd.DataFrame([\n",
    "#    {'observed_on': datetime(2020, 1, 1, 0, 0, 0, tzinfo=tz.tzlocal()), 'quality_grade': 'research'},\n",
    "#    {'observed_on': datetime(2020, 12, 31, 23, 59, 59, tzinfo=tz.tzlocal()), 'quality_grade': 'research'},\n",
    "#])\n",
    "#df = df.append(df2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Basic seasonality plot: observation counts by month & quality grade"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 455,
   "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>observed_month</th>\n",
       "      <th>quality_grade</th>\n",
       "      <th>counts</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>1.0</td>\n",
       "      <td>casual</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>3.0</td>\n",
       "      <td>casual</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>4.0</td>\n",
       "      <td>casual</td>\n",
       "      <td>4</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>4.0</td>\n",
       "      <td>research</td>\n",
       "      <td>29</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>5.0</td>\n",
       "      <td>casual</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>5.0</td>\n",
       "      <td>research</td>\n",
       "      <td>309</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>6.0</td>\n",
       "      <td>casual</td>\n",
       "      <td>20</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>6.0</td>\n",
       "      <td>research</td>\n",
       "      <td>1479</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>7.0</td>\n",
       "      <td>casual</td>\n",
       "      <td>21</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>7.0</td>\n",
       "      <td>needs_id</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>7.0</td>\n",
       "      <td>research</td>\n",
       "      <td>3325</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>8.0</td>\n",
       "      <td>casual</td>\n",
       "      <td>27</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>8.0</td>\n",
       "      <td>needs_id</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>8.0</td>\n",
       "      <td>research</td>\n",
       "      <td>3772</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>9.0</td>\n",
       "      <td>casual</td>\n",
       "      <td>34</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>9.0</td>\n",
       "      <td>needs_id</td>\n",
       "      <td>6</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>9.0</td>\n",
       "      <td>research</td>\n",
       "      <td>3023</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>10.0</td>\n",
       "      <td>casual</td>\n",
       "      <td>6</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>10.0</td>\n",
       "      <td>research</td>\n",
       "      <td>652</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>19</th>\n",
       "      <td>11.0</td>\n",
       "      <td>casual</td>\n",
       "      <td>5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>20</th>\n",
       "      <td>11.0</td>\n",
       "      <td>research</td>\n",
       "      <td>23</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>21</th>\n",
       "      <td>12.0</td>\n",
       "      <td>research</td>\n",
       "      <td>6</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "    observed_month quality_grade  counts\n",
       "0              1.0        casual       3\n",
       "1              3.0        casual       1\n",
       "2              4.0        casual       4\n",
       "3              4.0      research      29\n",
       "4              5.0        casual       2\n",
       "5              5.0      research     309\n",
       "6              6.0        casual      20\n",
       "7              6.0      research    1479\n",
       "8              7.0        casual      21\n",
       "9              7.0      needs_id       2\n",
       "10             7.0      research    3325\n",
       "11             8.0        casual      27\n",
       "12             8.0      needs_id       3\n",
       "13             8.0      research    3772\n",
       "14             9.0        casual      34\n",
       "15             9.0      needs_id       6\n",
       "16             9.0      research    3023\n",
       "17            10.0        casual       6\n",
       "18            10.0      research     652\n",
       "19            11.0        casual       5\n",
       "20            11.0      research      23\n",
       "21            12.0      research       6"
      ]
     },
     "execution_count": 455,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Preview: Show counts by month observed X quality grade\n",
    "df['observed_month'] = df['observed_on'].apply(lambda x: x.month)\n",
    "df[['observed_month', 'quality_grade']].groupby(['observed_month', 'quality_grade']).size().reset_index(name='counts')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 456,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<AxesSubplot:xlabel='observed_month', ylabel='Count'>"
      ]
     },
     "execution_count": 456,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 17092 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Plot the same data on a simple histogram\n",
    "sns.histplot(data=df, x='observed_month', hue='quality_grade', bins=12, discrete=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## More detailed seasonality plot: observation counts by month observed & time of day\n",
    "\n",
    "This plot uses a joint hexbin plot with marginal distributions.\n",
    "\n",
    "It attempts to answer the question \"When is the best time to see monarch butterfies in the Midwest US?\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 471,
   "metadata": {
    "tags": [
     "nbsphinx-thumbnail"
    ]
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<seaborn.axisgrid.JointGrid at 0x7f2e4edbbaf0>"
      ]
     },
     "execution_count": 471,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 120744 characters]\n",
      "text/plain": [
       "<Figure size 720x720 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "grid = sns.JointGrid(data=df, x='observed_on_mp', y='observed_time_mp', height=10, dropna=True)\n",
    "grid.ax_marg_x.set_title('Observation times of monarch butterflies in the Midwest US')\n",
    "\n",
    "# Format X axis labels & ticks\n",
    "xaxis = grid.ax_joint.get_xaxis()\n",
    "xaxis.label.set_text('Month')\n",
    "xaxis.set_major_locator(dates.DayLocator(interval=30))\n",
    "xaxis.set_major_formatter(dates.DateFormatter('%b %d'))\n",
    "#xaxis.set_minor_locator(dates.DayLocator(interval=7))\n",
    "#xaxis.set_minor_formatter(dates.DateFormatter('%d'))\n",
    "\n",
    "# Format Y axis labels & ticks\n",
    "yaxis = grid.ax_joint.get_yaxis()\n",
    "yaxis.label.set_text('Time of Day')\n",
    "yaxis.set_major_locator(dates.HourLocator(interval=2))\n",
    "yaxis.set_major_formatter(dates.DateFormatter('%H:%M'))\n",
    "#yaxis.set_minor_locator(dates.HourLocator())\n",
    "#yaxis.set_minor_formatter(dates.DateFormatter('%H:%M'))\n",
    "\n",
    "# Generate a joint plot with marginal plots\n",
    "# Using the hexbin plotting function, because hexagons are the bestagons.\n",
    "# Also because it looks just a little like butterfly scales.\n",
    "grid.plot_joint(plt.hexbin, gridsize=24, cmap=get_colormap(PLOT_COLOR))\n",
    "grid.plot_marginals(sns.histplot, color=PLOT_COLOR, kde=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Alternate version with shorter syntax but messier labels"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 475,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<seaborn.axisgrid.JointGrid at 0x7f2f0f291250>"
      ]
     },
     "execution_count": 475,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 18916 characters]\n",
      "text/plain": [
       "<Figure size 432x432 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "sns.jointplot(data=df, x='observed_on_mp', y='observed_time_mp', bins=24, kind='hist')"
   ]
  }
 ],
 "metadata": {
  "celltoolbar": "Tags",
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
