{ "cells": [ { "cell_type": "markdown", "id": "58d318e8-cf46-4834-af26-1f18ca84f093", "metadata": {}, "source": [ "# HW1 2022 Atlantic Tropical Cyclone Tracking Chart\n", "## 2022 Atlantic Tropical Cyclones Track Map\n", "### This notebook reads in historical tropical cyclone tracks from the [Hurdat2](https://www.nhc.noaa.gov/data/#hurdat) database; selects named storms that occurred in the Atlantic basin in 2022, and plots their tracks on a labeled map.\n", "\n", "#### Based on Metpy Monday Episodes [142](https://www.youtube.com/watch?v=AqZljJ1LsdA&t=66s&ab_channel=Unidata) and [143](https://www.youtube.com/watch?v=BRJb5rSjjDc&pp=ygUSTWV0cHkgbW9uZGF5cyAjMTQz)\n", "\n", "Notebook author: [Kevin Tyle](https://orcid.org/0000-0001-5249-9665)" ] }, { "cell_type": "markdown", "id": "d5cddcb9-d9c8-4f74-b36c-fad09694a747", "metadata": {}, "source": [ "
Keep in mind: This noteboook goes above and beyond what was expected from you for this assignment. Use it as a reference for your future work!
" ] }, { "cell_type": "markdown", "id": "706e13fc-0b79-4a33-b7fd-9617a590e463", "metadata": {}, "source": [ "#### Imports" ] }, { "cell_type": "code", "execution_count": null, "id": "ac666613-d6d1-40cb-aa2c-49181557a111", "metadata": { "tags": [] }, "outputs": [], "source": [ "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from datetime import datetime\n", "import cartopy.crs as ccrs\n", "import cartopy.feature as cfeature\n", "import matplotlib.pyplot as plt\n", "import matplotlib as mpl\n", "import numpy as np" ] }, { "cell_type": "markdown", "id": "893b6cae-ff84-4fb2-aa0e-6cae308f869a", "metadata": {}, "source": [ "#### Define a function to convert coordinates into floating point values" ] }, { "cell_type": "code", "execution_count": null, "id": "76f4ef5e-31a0-4328-8cf5-b3f5dca737f9", "metadata": { "tags": [] }, "outputs": [], "source": [ "def lat_lon_to_float(v):\n", " \"\"\"\n", " Convert strings from NHC to float locations\n", " \"\"\"\n", " if (v[-1] == 'S') or (v[-1] == 'W'):\n", " multiplier = -1\n", " else:\n", " multiplier = 1\n", " return float(v[:-1]) * multiplier" ] }, { "cell_type": "markdown", "id": "4556c177-0569-4ca2-9d17-7da68dc76212", "metadata": {}, "source": [ "#### Read in and parse the HURDAT2 data file, using built-in Python functions and the `datetime` library" ] }, { "cell_type": "code", "execution_count": null, "id": "e5810b79-03a0-4b1c-b41e-fcf1c2bf6824", "metadata": { "tags": [] }, "outputs": [], "source": [ "data = []\n", "with open('/spare11/atm533/data/hurdat2.txt', 'r') as f:\n", " for line in f.readlines():\n", " if line.startswith('AL'):\n", " storm_id = line.split(',')\n", " storm_number = storm_id[0].strip()\n", " storm_name = storm_id[1].strip()\n", " else:\n", " location_line = line.split(',')\n", " dt = datetime.strptime(location_line[0] + location_line[1], '%Y%m%d %H%M')\n", " storm_status = location_line[3].strip()\n", " storm_lat = lat_lon_to_float(location_line[4].strip())\n", " storm_lon = lat_lon_to_float(location_line[5].strip())\n", " max_speed = float(location_line[6].strip())\n", " data.append([storm_number, storm_name, storm_status, storm_lat,storm_lon, dt, max_speed])" ] }, { "cell_type": "markdown", "id": "c581c3db-56aa-446f-bb18-424b9531aa3b", "metadata": {}, "source": [ "Examine the first list element" ] }, { "cell_type": "code", "execution_count": null, "id": "0259ad42-245e-4761-bb48-8354feb0fa40", "metadata": { "tags": [] }, "outputs": [], "source": [ "data[0]" ] }, { "cell_type": "markdown", "id": "9b1835dc-7a75-46f0-8464-4737b83f82f5", "metadata": {}, "source": [ "#### Create a Pandas `dateframe` from the list, explicitly defining column names." ] }, { "cell_type": "code", "execution_count": null, "id": "53dccafa-7886-44dd-b624-1689a5382576", "metadata": { "tags": [] }, "outputs": [], "source": [ "df = pd.DataFrame(data, columns=['Storm Number', 'Storm Name', 'Storm Status', 'Lat', 'Lon', 'Time', 'Max Speed'])" ] }, { "cell_type": "markdown", "id": "34c095d6-f3ec-4ca5-bb30-35df4ec55530", "metadata": {}, "source": [ "Examine the `dataframe`" ] }, { "cell_type": "code", "execution_count": null, "id": "4114cd5d-c796-443f-80e5-6dc1a64dc201", "metadata": { "tags": [] }, "outputs": [], "source": [ "df" ] }, { "cell_type": "markdown", "id": "10e5b0e1-68c4-40b8-8f3f-c96230798b45", "metadata": {}, "source": [ "How many unique storms do we have?" ] }, { "cell_type": "code", "execution_count": null, "id": "256303c4-5792-4b59-9c8c-3ba95da802be", "metadata": { "tags": [] }, "outputs": [], "source": [ "len(df['Storm Number'].unique())" ] }, { "cell_type": "code", "execution_count": null, "id": "4bd4659f-ea70-455a-b40a-6add8129504a", "metadata": { "tags": [] }, "outputs": [], "source": [ "uniqueStorms = df['Storm Number'].unique()" ] }, { "cell_type": "code", "execution_count": null, "id": "50bb21b8-918a-4d18-9631-2c7b602f765b", "metadata": { "tags": [] }, "outputs": [], "source": [ "uniqueStorms" ] }, { "cell_type": "markdown", "id": "4951af9a-4604-44a0-855e-78c453badc70", "metadata": {}, "source": [ "Grouping by storm status, get the counts of each class of storm." ] }, { "cell_type": "code", "execution_count": null, "id": "ddc3e501-1e3c-478f-beef-f1b43564b8e9", "metadata": { "tags": [] }, "outputs": [], "source": [ "df.groupby('Storm Status').count()" ] }, { "cell_type": "markdown", "id": "2376b196-0447-439c-bc6e-59b0dd9c2c12", "metadata": {}, "source": [ "#### Get those storms that occurred in a desired year" ] }, { "cell_type": "code", "execution_count": null, "id": "d7bf2a71-1cce-409c-b49e-080d4e678ae5", "metadata": { "tags": [] }, "outputs": [], "source": [ "year = '2022'" ] }, { "cell_type": "code", "execution_count": null, "id": "8a400ae3-01c6-4656-a5c1-95cad5c75c91", "metadata": { "tags": [] }, "outputs": [], "source": [ "dfYear = df[df['Time'].dt.strftime(\"%Y\") == year]" ] }, { "cell_type": "code", "execution_count": null, "id": "7c3180ab-5930-4575-8638-9b0862e92462", "metadata": { "tags": [] }, "outputs": [], "source": [ "dfYear" ] }, { "cell_type": "markdown", "id": "056f5626-1c80-43f4-8152-01e44c7c1816", "metadata": {}, "source": [ "What are the unique storm names of this particular year?" ] }, { "cell_type": "code", "execution_count": null, "id": "ada73265-86e5-433c-b219-96bed3565f3b", "metadata": { "tags": [] }, "outputs": [], "source": [ "dfYear['Storm Name'].unique()" ] }, { "cell_type": "markdown", "id": "468c205c-902e-44c7-b283-90f77f99d995", "metadata": {}, "source": [ "Notice we have a couple of *unnamed* storms ... that is, storms that have been assigned a *numerical* name. These were tropical cyclones tracked by NHC (and thus in the HURDAT2 database) but never reached tropical storm status. We will want to omit these. We can do this by excluding those storms that never reached tropical storm, subtropical storm, or hurricane status." ] }, { "cell_type": "markdown", "id": "64d2de27-64fe-4f57-a6ee-b65103f07351", "metadata": { "tags": [] }, "source": [ "#### To make things easier to read and analyze, make the storm name the index of the dataframe." ] }, { "cell_type": "code", "execution_count": null, "id": "f78316fe-c565-4a01-ac86-fda19dd4a775", "metadata": { "tags": [] }, "outputs": [], "source": [ "dfYear = dfYear.set_index('Storm Name')\n", "dfYear" ] }, { "cell_type": "markdown", "id": "84bb9d8d-ec77-47d4-b9c1-c09baa803994", "metadata": {}, "source": [ "#### Iterate over each storm name, and check if the storm ever reached a named storm status. If so, add it to a list." ] }, { "cell_type": "code", "execution_count": null, "id": "c6640629-0ee1-4426-a101-69fd8adc0b47", "metadata": { "tags": [] }, "outputs": [], "source": [ "dfYear.index.unique()" ] }, { "cell_type": "code", "execution_count": null, "id": "f2c68167-f01f-4d02-b351-d2eacb409169", "metadata": { "tags": [] }, "outputs": [], "source": [ "keep = []\n", "for name in dfYear.index.unique():\n", " stormStatusSeries = dfYear.loc[name]['Storm Status']\n", " if stormStatusSeries.str.contains('DB|TS|SS').any():\n", " print(f'{name} was named')\n", " keep.append(name)\n", " else:\n", " print(f'{name} was NOT named')" ] }, { "cell_type": "code", "execution_count": null, "id": "9f28519b-7760-4c72-b187-49dff15321e1", "metadata": { "tags": [] }, "outputs": [], "source": [ "dfYear.loc['FIONA'].iloc[0].Lat" ] }, { "cell_type": "markdown", "id": "702a7e71-03fd-41c4-b234-ae675849fd70", "metadata": {}, "source": [ "Create a new `Dataframe` which now consists only of the named storms." ] }, { "cell_type": "code", "execution_count": null, "id": "62c58b65-0ab5-4cd8-9774-b74daefac397", "metadata": { "tags": [] }, "outputs": [], "source": [ "dfYearNamed = dfYear.loc[keep]" ] }, { "cell_type": "code", "execution_count": null, "id": "11d0cd2b-1e7a-4b69-b44e-c5fa615b7ff5", "metadata": { "tags": [] }, "outputs": [], "source": [ "dfYearNamed.index.unique()" ] }, { "cell_type": "markdown", "id": "327fd107-5145-4679-9796-a73a08217981", "metadata": {}, "source": [ "How many named storms do we have?" ] }, { "cell_type": "code", "execution_count": null, "id": "bd3989a2-5a94-425a-9361-2a58382f38a3", "metadata": { "tags": [] }, "outputs": [], "source": [ "nStorms = len(dfYearNamed.index.unique())\n", "nStorms" ] }, { "cell_type": "markdown", "id": "89e9b0eb-91f9-40e1-ac97-b3a640850655", "metadata": {}, "source": [ "### Plot the map" ] }, { "cell_type": "markdown", "id": "8a4135db-34d5-4ce4-85c0-aa73fa554cd9", "metadata": {}, "source": [ "Set the projection for the dataset and figure. The dataset uses latitude and longitude for its coordinates (**PlateCarree**). Given the large domain over which TCs may have tracked in a given year, use PlateCarree for the figure as well." ] }, { "cell_type": "code", "execution_count": null, "id": "d20c034a-e149-46e5-aa1d-436f9c646c77", "metadata": { "tags": [] }, "outputs": [], "source": [ "data_crs = ccrs.PlateCarree()\n", "plot_crs = ccrs.PlateCarree()" ] }, { "cell_type": "markdown", "id": "49247b71-7c20-4482-9842-bffeb22800a1", "metadata": {}, "source": [ "#### Color and linestyle considerations\n", "Distinguishing each track will be challenging. One strategy is to cycle through colors in a colormap and linestyles in a tuple (akin to a list). " ] }, { "cell_type": "code", "execution_count": null, "id": "6a0d689f-fab3-4325-bb21-156fc2f67898", "metadata": { "tags": [] }, "outputs": [], "source": [ "colors = mpl.cm.hsv(np.linspace(0, 1, nStorms))" ] }, { "cell_type": "markdown", "id": "84224b65-491f-45af-8b41-b7a9d3974264", "metadata": {}, "source": [ "
Note: hsv is one of the many colormaps available in Matplotlib.
" ] }, { "cell_type": "markdown", "id": "ee09e979-3b2a-4ae4-84e6-68319600b3b9", "metadata": {}, "source": [ "Create a tuple of Matplotlib linestyles. Repeat the pattern a number of times so we can ensure the number of linestyles is always greater than or equal to the number of storms." ] }, { "cell_type": "code", "execution_count": null, "id": "66840422-bfb0-4635-9c28-812f380035e4", "metadata": { "tags": [] }, "outputs": [], "source": [ "linestyle_tuple = 100*[\n", " ('loosely dotted', (0, (1, 10))),\n", " ('dotted', (0, (1, 1))),\n", " ('densely dotted', (0, (1, 1))),\n", " ('long dash with offset', (5, (10, 3))),\n", " ('loosely dashed', (0, (5, 10))),\n", " ('dashed', (0, (5, 5))),\n", " ('densely dashed', (0, (5, 1))),\n", "\n", " ('loosely dashdotted', (0, (3, 10, 1, 10))),\n", " ('dashdotted', (0, (3, 5, 1, 5))),\n", " ('densely dashdotted', (0, (3, 1, 1, 1))),\n", "\n", " ('dashdotdotted', (0, (3, 5, 1, 5, 1, 5))),\n", " ('loosely dashdotdotted', (0, (3, 10, 1, 10, 1, 10))),\n", " ('densely dashdotdotted', (0, (3, 1, 1, 1, 1, 1)))]" ] }, { "cell_type": "code", "execution_count": null, "id": "f62de28b-197e-4c96-899d-3a4818d89424", "metadata": { "tags": [] }, "outputs": [], "source": [ "linestyles = linestyle_tuple[::1][:nStorms]" ] }, { "cell_type": "code", "execution_count": null, "id": "2d12a20f-d0d1-4169-a889-c5f71620553e", "metadata": {}, "outputs": [], "source": [ "# Use medium-scale cartographic features\n", "res = '50m'\n", "\n", "fig = plt.figure(figsize=(15,12), dpi=150)\n", "ax = plt.subplot(1,1,1, projection=plot_crs)\n", "\n", "ax.set_extent([-150, 0, 5, 65], data_crs)\n", "ax.set_facecolor(cfeature.COLORS['water'])\n", "ax.add_feature (cfeature.STATES.with_scale(res))\n", "ax.add_feature (cfeature.LAND.with_scale(res))\n", "ax.add_feature (cfeature.COASTLINE.with_scale(res))\n", "ax.add_feature (cfeature.LAKES.with_scale(res))\n", "ax.add_feature (cfeature.STATES.with_scale(res))\n", "\n", "# Loop over each storm\n", "for idx, storm_name in enumerate(dfYearNamed.index.unique()):\n", "\n", " storm_data = dfYearNamed[dfYearNamed.index == storm_name]\n", " track_len = len(storm_data)\n", " ax.plot(storm_data['Lon'], storm_data['Lat'], transform=data_crs,\n", " label=storm_name,c=colors[idx],linestyle=linestyles[idx][1])\n", " s = ax.scatter(storm_data['Lon'], storm_data['Lat'], transform=data_crs, c=storm_data['Max Speed'], \n", " vmin=40, vmax=150, s=storm_data['Max Speed']/5, cmap='Reds')\n", "# Get the coordinates of the first point in each storm's track, to be used for labeling\n", " lon0, lat0 = storm_data['Lon'].iloc[0], storm_data['Lat'].iloc[0]\n", " ax.text(lon0, lat0, s=storm_name,fontdict=dict(color='black', size=6), bbox=dict(facecolor='#CCCCCC', alpha=0.5))\n", "\n", "\n", " ax.legend(loc='upper left', fontsize=7)\n", "\n", "# Once out of the loop, create and label a colorbar, set the title, and save the figure.\n", "cbar = plt.colorbar(s,shrink=0.35)\n", "cbar.ax.tick_params(labelsize=12)\n", "cbar.ax.set_ylabel(\"Windspeed (kts)\",fontsize=13);\n", "\n", "plotTitle = f'Named Tropical / Subtropical Cyclones, Atlantic Basin, {year}'\n", "ax.set_title (plotTitle);\n", "\n", "fig.savefig (f'AtlTCs{year}.png')" ] }, { "cell_type": "markdown", "id": "4503fe2f-0a27-4f3c-9a53-6b00b171e7c3", "metadata": {}, "source": [ "#### Create another map, this time centered over a smaller region." ] }, { "cell_type": "code", "execution_count": null, "id": "499446aa-fa0c-4e6b-9f7a-95b3266cbd82", "metadata": { "tags": [] }, "outputs": [], "source": [ "regLonW, regLonE, regLatS, regLatN = (-95, -50, 5, 35)" ] }, { "cell_type": "code", "execution_count": null, "id": "f2f1c463-dcc2-461e-ac14-18983b941f72", "metadata": {}, "outputs": [], "source": [ "# Use medium-scale cartographic features\n", "res = '50m'\n", "\n", "fig = plt.figure(figsize=(15,12), dpi=150)\n", "ax = plt.subplot(1,1,1, projection=plot_crs)\n", "\n", "ax.set_extent([regLonW, regLonE, regLatS, regLatN], data_crs)\n", "ax.set_facecolor(cfeature.COLORS['water'])\n", "ax.add_feature (cfeature.STATES.with_scale(res))\n", "ax.add_feature (cfeature.LAND.with_scale(res))\n", "ax.add_feature (cfeature.COASTLINE.with_scale(res))\n", "ax.add_feature (cfeature.LAKES.with_scale(res))\n", "ax.add_feature (cfeature.STATES.with_scale(res))\n", "\n", "# Loop over each storm\n", "for idx, storm_name in enumerate(dfYearNamed.index.unique()):\n", "\n", " storm_data = dfYearNamed[dfYearNamed.index == storm_name]\n", " track_len = len(storm_data)\n", " ax.plot(storm_data['Lon'], storm_data['Lat'], transform=data_crs,\n", " label=storm_name,c=colors[idx],linestyle=linestyles[idx][1])\n", " s = ax.scatter(storm_data['Lon'], storm_data['Lat'], transform=data_crs, c=storm_data['Max Speed'], \n", " vmin=40, vmax=150, s=storm_data['Max Speed']/5, cmap='Reds')\n", "# Get the coordinates of the first point in each storm's track, to be used for labeling\n", "\n", " lon0, lat0 = storm_data['Lon'].iloc[0], storm_data['Lat'].iloc[0]\n", "# Ensure that we only plot the text box if the coordinates are within the regional extent\n", " if (regLonW < lon0 < regLonE and regLatS < lat0 < regLatN):\n", " ax.text(lon0, lat0, s=storm_name,fontdict=dict(color='black', size=6), bbox=dict(facecolor='#CCCCCC', alpha=0.5))\n", "\n", "\n", " ax.legend(loc='upper left', fontsize=7)\n", "\n", "# Once out of the loop, create and label a colorbar, set the title, and save the figure.\n", "cbar = plt.colorbar(s,shrink=0.55)\n", "cbar.ax.tick_params(labelsize=12)\n", "cbar.ax.set_ylabel(\"Windspeed (kts)\",fontsize=13);\n", "\n", "plotTitle = f'Named Tropical / Subtropical Cyclones, Atlantic Basin (Regional subset) {year}'\n", "ax.set_title (plotTitle);\n", "\n", "fig.savefig (f'AtlTCs{year}Regional.png')" ] }, { "cell_type": "markdown", "id": "3a0dc09d-625f-4aaa-adad-1a2055b891ae", "metadata": {}, "source": [ "## Summary" ] }, { "cell_type": "markdown", "id": "3ee887dc-176c-4f8b-a1e4-84285510099b", "metadata": { "tags": [] }, "source": [ "We've done a lot of tweaking to produce a fairly self-explanatory tropical cyclone tracking chart ... but as always there's room to further improve!" ] }, { "cell_type": "markdown", "id": "3d41dc2c-8b3d-4dd4-9dcc-db85fcd3d93c", "metadata": {}, "source": [ "## References\n", "1. [Maptlotlib line styles](https://matplotlib.org/stable/gallery/lines_bars_and_markers/linestyles.html)\n", "1. [Matplotlib color maps](https://matplotlib.org/stable/users/explain/colors/colormaps.html)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 August 2023 Environment", "language": "python", "name": "aug23" }, "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 }