Exploring the NHC Advisories and Sea Surface Height during Hurricane Irma

This notebook aims to demonstrate how to create a simple interactive GIS map with the National Hurricane Center predictions [1] and the observed sea surface height from CO-OPS [2].

See https://tidesandcurrents.noaa.gov/quicklook/view.html?name=IRMA for the latest information on Irma.

  1. http://www.nhc.noaa.gov/gis/

  2. https://opendap.co-ops.nos.noaa.gov/ioos-dif-sos/

First we have to download the National Hurricane Center (NHC) GIS 5 day predictions data for Irma.

NHC codes storms are coded with 8 letter names:

  • 2 char for region al → Atlantic

  • 2 char for number 11 is Irma

  • and 4 char for year, 2017

Browse http://www.nhc.noaa.gov/gis/archive_wsurge.php?year=2017 to find other hurricanes code.

code = "al112017"
hurricane = "{}_5day".format(code)
import os
import sys

import lxml.html

try:
    from urllib.request import urlopen, urlretrieve
except Exception:
    from urllib import urlopen, urlretrieve


def url_lister(url):
    urls = []
    connection = urlopen(url)
    dom = lxml.html.fromstring(connection.read())
    for link in dom.xpath("//a/@href"):
        urls.append(link)
    return urls


def download(url, path):
    sys.stdout.write(fname + "\n")
    if not os.path.isfile(path):
        urlretrieve(url, filename=path, reporthook=progress_hook(sys.stdout))
        sys.stdout.write("\n")
        sys.stdout.flush()


def progress_hook(out):
    """
    Return a progress hook function, suitable for passing to
    urllib.retrieve, that writes to the file object *out*.
    """

    def it(n, bs, ts):
        got = n * bs
        if ts < 0:
            outof = ""
        else:
            # On the last block n*bs can exceed ts, so we clamp it
            # to avoid awkward questions.
            got = min(got, ts)
            outof = "/%d [%d%%]" % (ts, 100 * got // ts)
        out.write("\r  %d%s" % (got, outof))
        out.flush()

    return it
nhc = "http://www.nhc.noaa.gov/gis/forecast/archive/"

# We don't need the latest file b/c that is redundant to the latest number.
fnames = [
    fname
    for fname in url_lister(nhc)
    if fname.startswith(hurricane) and "latest" not in fname
]
base = os.path.abspath(os.path.join(os.path.curdir, "data", hurricane))

if not os.path.exists(base):
    os.makedirs(base)

for fname in fnames:
    url = "{}/{}".format(nhc, fname)
    path = os.path.join(base, fname)
    download(url, path)
al112017_5day_001.zip
al112017_5day_002.zip
al112017_5day_003.zip
al112017_5day_004.zip
al112017_5day_005.zip
al112017_5day_006.zip
al112017_5day_007.zip
al112017_5day_008.zip
al112017_5day_009.zip
al112017_5day_010.zip
al112017_5day_011.zip
al112017_5day_012.zip
al112017_5day_013.zip
al112017_5day_014.zip
al112017_5day_015.zip
al112017_5day_016.zip
al112017_5day_017.zip
al112017_5day_018.zip
al112017_5day_018A.zip
al112017_5day_019.zip
al112017_5day_019A.zip
al112017_5day_020.zip
al112017_5day_020A.zip
al112017_5day_021.zip
al112017_5day_021A.zip
al112017_5day_022.zip
al112017_5day_022A.zip
al112017_5day_023.zip
al112017_5day_023A.zip
al112017_5day_024.zip
al112017_5day_025.zip
al112017_5day_026.zip
al112017_5day_026A.zip
al112017_5day_027.zip
al112017_5day_027A.zip
al112017_5day_028.zip
al112017_5day_028A.zip
al112017_5day_029.zip
al112017_5day_029A.zip
al112017_5day_030.zip
al112017_5day_030A.zip
al112017_5day_031.zip
al112017_5day_031A.zip
al112017_5day_032.zip
al112017_5day_032A.zip
al112017_5day_033.zip
al112017_5day_033A.zip
al112017_5day_034.zip
al112017_5day_034A.zip
al112017_5day_035.zip
al112017_5day_035A.zip
al112017_5day_036.zip
al112017_5day_036A.zip
al112017_5day_037.zip
al112017_5day_037A.zip
al112017_5day_038.zip
al112017_5day_038A.zip
al112017_5day_039.zip
al112017_5day_039A.zip
al112017_5day_040.zip
al112017_5day_040A.zip
al112017_5day_041.zip
al112017_5day_041A.zip
al112017_5day_042.zip
al112017_5day_042A.zip
al112017_5day_043.zip
al112017_5day_043A.zip
al112017_5day_044.zip
al112017_5day_044A.zip
al112017_5day_045.zip
al112017_5day_045A.zip
al112017_5day_046.zip
al112017_5day_046A.zip
al112017_5day_047.zip
al112017_5day_047A.zip
al112017_5day_048.zip
al112017_5day_048A.zip
al112017_5day_049.zip
al112017_5day_049A.zip
al112017_5day_050.zip
al112017_5day_050A.zip
al112017_5day_051.zip
al112017_5day_051A.zip
al112017_5day_052.zip

In the cells below we use geopandas to load the data we just downloaded. We will use only the prediction cone (png) and the track points (pts), but feel free to explore this data further, there is plenty more there.

import os

os.environ["CPL_ZIP_ENCODING"] = "UTF-8"
os.environ["TZ"] = "GMT0"
from glob import glob

import geopandas

cones, points = [], []
for fname in sorted(glob(os.path.join(base, "{}_*.zip".format(hurricane)))):
    number = os.path.splitext(os.path.split(fname)[-1])[0].split("_")[-1]
    pgn = geopandas.read_file(
        "/{}-{}_5day_pgn.shp".format(code, number), vfs="zip://{}".format(fname)
    )
    cones.append(pgn)

    pts = geopandas.read_file(
        "/{}-{}_5day_pts.shp".format(code, number), vfs="zip://{}".format(fname)
    )
    # Only the first "obsevartion."
    points.append(pts.iloc[0])

Let’s create a color code for the point track.

colors = {
    "Subtropical Depression": "yellow",
    "Tropical Depression": "yellow",
    "Tropical Storm": "orange",
    "Subtropical Storm": "orange",
    "Hurricane": "red",
    "Major Hurricane": "crimson",
}

Now we can get all the information we need from those GIS files. Let’s start with the event dates.

import dateutil

start = points[0]["FLDATELBL"].strip(" AST")
end = points[-1]["FLDATELBL"].strip(" EDT")

start = dateutil.parser.parse(start)
end = dateutil.parser.parse(end)

And the bounding box to search the data.

from shapely.geometry import LineString
from shapely.ops import cascaded_union

last_cone = cones[-1]["geometry"].iloc[0]
track = LineString([point["geometry"] for point in points])

polygon = cascaded_union([last_cone, track])

# Add a buffer to find the stations along the track.
bbox = polygon.buffer(2).bounds

Note that the bounding box is derived from the track and the latest prediction cone.

strbbox = ", ".join(format(v, ".2f") for v in bbox)
print("bbox: {}\nstart: {}\n  end: {}".format(strbbox, start, end))
bbox: -91.91, 14.40, -28.30, 39.45
start: 2017-08-30 08:00:00
  end: 2017-09-11 20:00:00

Now we need to build a filter with those parameters to find the observations along the Hurricane path. We still need to specify:

  • the units for the observations;

  • and the SOS name for the variables of interest.

Next, we can use pyoos to assemble a collector to download the data into a pandas DataFrame.

import cf_units
import pandas as pd
from ioos_tools.ioos import collector2table
from pyoos.collectors.coops.coops_sos import CoopsSos
from retrying import retry


# We need to retry in case of failure b/c the server cannot handle
# the high traffic during events like Irma.
@retry(stop_max_attempt_number=5, wait_fixed=3000)
def get_coops(start, end, sos_name, units, bbox, verbose=False):
    collector = CoopsSos()
    collector.set_bbox(bbox)
    collector.end_time = end
    collector.start_time = start
    collector.variables = [sos_name]
    ofrs = collector.server.offerings
    title = collector.server.identification.title
    config = dict(units=units, sos_name=sos_name,)

    data = collector2table(
        collector=collector,
        config=config,
        col="{} ({})".format(sos_name, units.format(cf_units.UT_ISO_8859_1)),
    )

    # Clean the table.
    table = dict(
        station_name=[s._metadata.get("station_name") for s in data],
        station_code=[s._metadata.get("station_code") for s in data],
        sensor=[s._metadata.get("sensor") for s in data],
        lon=[s._metadata.get("lon") for s in data],
        lat=[s._metadata.get("lat") for s in data],
        depth=[s._metadata.get("depth", "NA") for s in data],
    )

    table = pd.DataFrame(table).set_index("station_name")
    if verbose:
        print("Collector offerings")
        print("{}: {} offerings".format(title, len(ofrs)))
    return data, table
ssh, ssh_table = get_coops(
    start=start,
    end=end,
    sos_name="water_surface_height_above_reference_datum",
    units=cf_units.Unit("meters"),
    bbox=bbox,
)

ssh_table
station_code sensor lon lat depth
station_name
Bermuda Biological Station 2695535 urn:ioos:sensor:NOAA.NOS.CO-OPS:2695535:N1 -64.6950 32.3700 None
Bermuda, St. Georges Island, Bermuda 2695540 urn:ioos:sensor:NOAA.NOS.CO-OPS:2695540:Y1 -64.7033 32.3733 None
Atlantic City, NJ 8534720 urn:ioos:sensor:NOAA.NOS.CO-OPS:8534720:A1 -74.4181 39.3567 None
Cape May, NJ 8536110 urn:ioos:sensor:NOAA.NOS.CO-OPS:8536110:A1 -74.9597 38.9678 None
Ship John Shoal, NJ 8537121 urn:ioos:sensor:NOAA.NOS.CO-OPS:8537121:A1 -75.3750 39.3050 None
Brandywine Shoal Light, DE 8555889 urn:ioos:sensor:NOAA.NOS.CO-OPS:8555889:Y1 -75.1130 38.9870 None
Lewes, DE 8557380 urn:ioos:sensor:NOAA.NOS.CO-OPS:8557380:A1 -75.1192 38.7828 None
Ocean City Inlet, MD 8570283 urn:ioos:sensor:NOAA.NOS.CO-OPS:8570283:A1 -75.0911 38.3283 None
Bishops Head, MD 8571421 urn:ioos:sensor:NOAA.NOS.CO-OPS:8571421:A1 -76.0387 38.2204 None
Cambridge, MD 8571892 urn:ioos:sensor:NOAA.NOS.CO-OPS:8571892:Y1 -76.0617 38.5725 None
Tolchester Beach, MD 8573364 urn:ioos:sensor:NOAA.NOS.CO-OPS:8573364:A1 -76.2450 39.2133 None
Baltimore, Fort McHenry, Patapsco River, MD 8574680 urn:ioos:sensor:NOAA.NOS.CO-OPS:8574680:A1 -76.5794 39.2669 None
Annapolis, MD 8575512 urn:ioos:sensor:NOAA.NOS.CO-OPS:8575512:A1 -76.4816 38.9833 None
Solomons Island, MD 8577330 urn:ioos:sensor:NOAA.NOS.CO-OPS:8577330:Y1 -76.4508 38.3172 None
Washington, DC 8594900 urn:ioos:sensor:NOAA.NOS.CO-OPS:8594900:A1 -77.0217 38.8730 None
Wachapreague, VA 8631044 urn:ioos:sensor:NOAA.NOS.CO-OPS:8631044:Y1 -75.6858 37.6078 None
Kiptopeke, VA 8632200 urn:ioos:sensor:NOAA.NOS.CO-OPS:8632200:A1 -75.9884 37.1652 None
Dahlgren, VA 8635027 urn:ioos:sensor:NOAA.NOS.CO-OPS:8635027:Y1 -77.0366 38.3197 None
Lewisetta, VA 8635750 urn:ioos:sensor:NOAA.NOS.CO-OPS:8635750:A1 -76.4647 37.9953 None
Windmill Point, VA 8636580 urn:ioos:sensor:NOAA.NOS.CO-OPS:8636580:Y1 -76.2900 37.6161 None
Yorktown USCG Training Center, VA 8637689 urn:ioos:sensor:NOAA.NOS.CO-OPS:8637689:A1 -76.4788 37.2265 None
Sewells Point, VA 8638610 urn:ioos:sensor:NOAA.NOS.CO-OPS:8638610:A1 -76.3300 36.9467 None
CBBT, Chesapeake Channel, VA 8638901 urn:ioos:sensor:NOAA.NOS.CO-OPS:8638901:Y1 -76.0833 37.0329 None
Money Point, VA 8639348 urn:ioos:sensor:NOAA.NOS.CO-OPS:8639348:Y1 -76.3017 36.7783 None
Duck, NC 8651370 urn:ioos:sensor:NOAA.NOS.CO-OPS:8651370:A1 -75.7467 36.1833 None
Oregon Inlet Marina, NC 8652587 urn:ioos:sensor:NOAA.NOS.CO-OPS:8652587:A1 -75.5481 35.7950 None
USCG Station Hatteras, NC 8654467 urn:ioos:sensor:NOAA.NOS.CO-OPS:8654467:A1 -75.7042 35.2086 None
Beaufort, Duke Marine Lab, NC 8656483 urn:ioos:sensor:NOAA.NOS.CO-OPS:8656483:Y1 -76.6700 34.7200 None
Wilmington, NC 8658120 urn:ioos:sensor:NOAA.NOS.CO-OPS:8658120:A1 -77.9536 34.2275 None
Wrightsville Beach, NC 8658163 urn:ioos:sensor:NOAA.NOS.CO-OPS:8658163:A1 -77.7867 34.2133 None
... ... ... ... ... ...
West Fowl River Bridge, AL 8738043 urn:ioos:sensor:NOAA.NOS.CO-OPS:8738043:Y1 -88.1586 30.3766 None
Bayou La Batre Bridge, AL 8739803 urn:ioos:sensor:NOAA.NOS.CO-OPS:8739803:Y1 -88.2478 30.4062 None
Pascagoula NOAA Lab, MS 8741533 urn:ioos:sensor:NOAA.NOS.CO-OPS:8741533:A1 -88.5631 30.3678 None
Bay Waveland Yacht Club, MS 8747437 urn:ioos:sensor:NOAA.NOS.CO-OPS:8747437:A1 -89.3250 30.3250 None
Pilottown, LA 8760721 urn:ioos:sensor:NOAA.NOS.CO-OPS:8760721:A1 -89.2583 29.1783 None
Pilots Station East, S.W. Pass, LA 8760922 urn:ioos:sensor:NOAA.NOS.CO-OPS:8760922:Y1 -89.4075 28.9322 None
Shell Beach, LA 8761305 urn:ioos:sensor:NOAA.NOS.CO-OPS:8761305:A1 -89.6730 29.8683 None
Grand Isle, LA 8761724 urn:ioos:sensor:NOAA.NOS.CO-OPS:8761724:A1 -89.9567 29.2633 None
New Canal Station, LA 8761927 urn:ioos:sensor:NOAA.NOS.CO-OPS:8761927:A1 -90.1133 30.0272 None
Carrollton, LA 8761955 urn:ioos:sensor:NOAA.NOS.CO-OPS:8761955:A1 -90.1355 29.9329 None
Port Fourchon, Belle Pass, LA 8762075 urn:ioos:sensor:NOAA.NOS.CO-OPS:8762075:A1 -90.1993 29.1142 None
West Bank 1, Bayou Gauche, LA 8762482 urn:ioos:sensor:NOAA.NOS.CO-OPS:8762482:Y1 -90.4203 29.7886 None
I-10 Bonnet Carre Floodway, LA 8762483 urn:ioos:sensor:NOAA.NOS.CO-OPS:8762483:Y1 -90.3900 30.0679 None
Berwick, Atchafalaya River, LA 8764044 urn:ioos:sensor:NOAA.NOS.CO-OPS:8764044:A1 -91.2376 29.6675 None
LAWMA, Amerada Pass, LA 8764227 urn:ioos:sensor:NOAA.NOS.CO-OPS:8764227:Y1 -91.3381 29.4496 None
Eugene Island, North of , Gulf of Mexico, LA 8764314 urn:ioos:sensor:NOAA.NOS.CO-OPS:8764314:Y1 -91.3839 29.3675 None
Christiansted Harbor, St Croix, VI 9751364 urn:ioos:sensor:NOAA.NOS.CO-OPS:9751364:A1 -64.6984 17.7477 None
Lameshur Bay, St John, VI 9751381 urn:ioos:sensor:NOAA.NOS.CO-OPS:9751381:A1 -64.7242 18.3182 None
Lime Tree Bay, VI 9751401 urn:ioos:sensor:NOAA.NOS.CO-OPS:9751401:Y1 -64.7538 17.6947 None
Charlotte Amalie, VI 9751639 urn:ioos:sensor:NOAA.NOS.CO-OPS:9751639:Y1 -64.9258 18.3472 None
Culebra, PR 9752235 urn:ioos:sensor:NOAA.NOS.CO-OPS:9752235:NT -65.3025 18.3009 None
Isabel Segunda, Vieques Island, PR 9752619 urn:ioos:sensor:NOAA.NOS.CO-OPS:9752619:A1 -65.4439 18.1525 None
Esperanza, Vieques Island, PR 9752695 urn:ioos:sensor:NOAA.NOS.CO-OPS:9752695:A1 -65.4714 18.0939 None
Yabucoa Harbor, PR 9754228 urn:ioos:sensor:NOAA.NOS.CO-OPS:9754228:A1 -65.8330 18.0551 None
San Juan, La Puntilla, San Juan Bay, PR 9755371 urn:ioos:sensor:NOAA.NOS.CO-OPS:9755371:Y1 -66.1164 18.4592 None
Arecibo, PR 9757809 urn:ioos:sensor:NOAA.NOS.CO-OPS:9757809:A1 -66.7024 18.4805 None
Magueyes Island, PR 9759110 urn:ioos:sensor:NOAA.NOS.CO-OPS:9759110:A1 -67.0464 17.9701 None
Mayaguez, PR 9759394 urn:ioos:sensor:NOAA.NOS.CO-OPS:9759394:Y1 -67.1625 18.2190 None
Mona Island, PR 9759938 urn:ioos:sensor:NOAA.NOS.CO-OPS:9759938:N1 -67.9385 18.0899 None
Barbuda, Antigua and Barbuda 9761115 urn:ioos:sensor:NOAA.NOS.CO-OPS:9761115:N1 -61.8206 17.5908 None

93 rows × 5 columns

wind_speed, wind_speed_table = get_coops(
    start=start, end=end, sos_name="wind_speed", units=cf_units.Unit("m/s"), bbox=bbox,
)

wind_speed_table
station_code sensor lon lat depth
station_name
Bermuda, St. Georges Island, Bermuda 2695540 urn:ioos:sensor:NOAA.NOS.CO-OPS:2695540:C1 -64.7033 32.3734 None
Cape May, NJ 8536110 urn:ioos:sensor:NOAA.NOS.CO-OPS:8536110:C1 -74.9597 38.9678 None
Ship John Shoal, NJ 8537121 urn:ioos:sensor:NOAA.NOS.CO-OPS:8537121:C1 -75.3750 39.3050 None
Brandywine Shoal Light, DE 8555889 urn:ioos:sensor:NOAA.NOS.CO-OPS:8555889:C1 -75.1133 38.9867 None
Lewes, DE 8557380 urn:ioos:sensor:NOAA.NOS.CO-OPS:8557380:C1 -75.1192 38.7828 None
Ocean City Inlet, MD 8570283 urn:ioos:sensor:NOAA.NOS.CO-OPS:8570283:C1 -75.0917 38.3283 None
Bishops Head, MD 8571421 urn:ioos:sensor:NOAA.NOS.CO-OPS:8571421:C1 -76.0387 38.2204 None
Tolchester Beach, MD 8573364 urn:ioos:sensor:NOAA.NOS.CO-OPS:8573364:C1 -76.2450 39.2133 None
Baltimore, Fort McHenry, Patapsco River, MD 8574680 urn:ioos:sensor:NOAA.NOS.CO-OPS:8574680:C1 -76.5783 39.2667 None
Francis Scott Key Bridge, MD 8574728 urn:ioos:sensor:NOAA.NOS.CO-OPS:8574728:C1 -76.5283 39.2200 None
Cove Point LNG Pier, MD 8577018 urn:ioos:sensor:NOAA.NOS.CO-OPS:8577018:C1 -76.3855 38.4044 None
Solomons Island, MD 8577330 urn:ioos:sensor:NOAA.NOS.CO-OPS:8577330:C1 -76.4508 38.3172 None
Piney Point, MD 8578240 urn:ioos:sensor:NOAA.NOS.CO-OPS:8578240:C1 -76.5333 38.1333 None
Washington, DC 8594900 urn:ioos:sensor:NOAA.NOS.CO-OPS:8594900:C1 -77.0217 38.8733 None
Wachapreague, VA 8631044 urn:ioos:sensor:NOAA.NOS.CO-OPS:8631044:C1 -75.6858 37.6078 None
Kiptopeke, VA 8632200 urn:ioos:sensor:NOAA.NOS.CO-OPS:8632200:C1 -75.9884 37.1652 None
Rappahannock Light, VA 8632837 urn:ioos:sensor:NOAA.NOS.CO-OPS:8632837:C1 -76.0150 37.5383 None
Dahlgren, VA 8635027 urn:ioos:sensor:NOAA.NOS.CO-OPS:8635027:C1 -77.0366 38.3197 None
Lewisetta, VA 8635750 urn:ioos:sensor:NOAA.NOS.CO-OPS:8635750:C1 -76.4646 37.9954 None
York River East Rear Range Light, VA 8637611 urn:ioos:sensor:NOAA.NOS.CO-OPS:8637611:C1 -76.3333 37.2500 None
Yorktown USCG Training Center, VA 8637689 urn:ioos:sensor:NOAA.NOS.CO-OPS:8637689:C1 -76.4788 37.2265 None
Dominion Terminal Associates, VA 8638511 urn:ioos:sensor:NOAA.NOS.CO-OPS:8638511:C1 -76.4242 36.9623 None
South Craney Island, VA 8638595 urn:ioos:sensor:NOAA.NOS.CO-OPS:8638595:C1 -76.3383 36.8883 None
Willoughby Degaussing Station, VA 8638614 urn:ioos:sensor:NOAA.NOS.CO-OPS:8638614:C1 -76.3217 36.9817 None
urn:ioos:station:NOAA.NOS.CO-OPS:8638863 8638863 urn:ioos:sensor:NOAA.NOS.CO-OPS:8638863:C1 -76.1133 36.9667 None
CBBT, Chesapeake Channel, VA 8638901 urn:ioos:sensor:NOAA.NOS.CO-OPS:8638901:C1 -76.0833 37.0329 None
Cape Henry, VA 8638999 urn:ioos:sensor:NOAA.NOS.CO-OPS:8638999:C1 -76.0067 36.9300 None
Money Point, VA 8639348 urn:ioos:sensor:NOAA.NOS.CO-OPS:8639348:C1 -76.3017 36.7783 None
Duck, NC 8651370 urn:ioos:sensor:NOAA.NOS.CO-OPS:8651370:C1 -75.7467 36.1833 None
Oregon Inlet Marina, NC 8652587 urn:ioos:sensor:NOAA.NOS.CO-OPS:8652587:C1 -75.5481 35.7950 None
... ... ... ... ... ...
Panama City, FL 8729108 urn:ioos:sensor:NOAA.NOS.CO-OPS:8729108:C1 -85.6669 30.1523 None
Panama City Beach, FL 8729210 urn:ioos:sensor:NOAA.NOS.CO-OPS:8729210:C1 -85.8783 30.2133 None
Fort Morgan, AL 8734673 urn:ioos:sensor:NOAA.NOS.CO-OPS:8734673:C1 -88.0250 30.2283 None
Dauphin Island, AL 8735180 urn:ioos:sensor:NOAA.NOS.CO-OPS:8735180:C1 -88.0750 30.2500 None
Coast Guard Sector Mobile, AL 8736897 urn:ioos:sensor:NOAA.NOS.CO-OPS:8736897:C1 -88.0583 30.6483 None
Petit Bois Island, Port of Pascagoula, MS 8741003 urn:ioos:sensor:NOAA.NOS.CO-OPS:8741003:C1 -88.5000 30.2133 None
urn:ioos:station:NOAA.NOS.CO-OPS:8741094 8741094 urn:ioos:sensor:NOAA.NOS.CO-OPS:8741094:C1 -88.5117 30.3433 None
urn:ioos:station:NOAA.NOS.CO-OPS:8741501 8741501 urn:ioos:sensor:NOAA.NOS.CO-OPS:8741501:C1 -88.5667 30.3550 None
Bay Waveland Yacht Club, MS 8747437 urn:ioos:sensor:NOAA.NOS.CO-OPS:8747437:C1 -89.3250 30.3250 None
Pilottown, LA 8760721 urn:ioos:sensor:NOAA.NOS.CO-OPS:8760721:C1 -89.2583 29.1783 None
Pilots Station East, S.W. Pass, LA 8760922 urn:ioos:sensor:NOAA.NOS.CO-OPS:8760922:C1 -89.4075 28.9322 None
Shell Beach, LA 8761305 urn:ioos:sensor:NOAA.NOS.CO-OPS:8761305:C1 -89.6732 29.8681 None
Grand Isle, LA 8761724 urn:ioos:sensor:NOAA.NOS.CO-OPS:8761724:C1 -89.9567 29.2633 None
New Canal Station, LA 8761927 urn:ioos:sensor:NOAA.NOS.CO-OPS:8761927:C1 -90.1133 30.0272 None
West Bank 1, Bayou Gauche, LA 8762482 urn:ioos:sensor:NOAA.NOS.CO-OPS:8762482:C1 -90.4203 29.7886 None
Berwick, Atchafalaya River, LA 8764044 urn:ioos:sensor:NOAA.NOS.CO-OPS:8764044:C1 -91.2376 29.6675 None
LAWMA, Amerada Pass, LA 8764227 urn:ioos:sensor:NOAA.NOS.CO-OPS:8764227:C1 -91.3381 29.4496 None
Eugene Island, North of , Gulf of Mexico, LA 8764314 urn:ioos:sensor:NOAA.NOS.CO-OPS:8764314:C1 -91.3839 29.3675 None
Christiansted Harbor, St Croix, VI 9751364 urn:ioos:sensor:NOAA.NOS.CO-OPS:9751364:C1 -64.7050 17.7500 None
Lime Tree Bay, VI 9751401 urn:ioos:sensor:NOAA.NOS.CO-OPS:9751401:C1 -64.7538 17.6947 None
Charlotte Amalie, VI 9751639 urn:ioos:sensor:NOAA.NOS.CO-OPS:9751639:C1 -64.9200 18.3358 None
Isabel Segunda, Vieques Island, PR 9752619 urn:ioos:sensor:NOAA.NOS.CO-OPS:9752619:C1 -65.4438 18.1525 None
Esperanza, Vieques Island, PR 9752695 urn:ioos:sensor:NOAA.NOS.CO-OPS:9752695:C1 -65.4714 18.0939 None
Fajardo, PR 9753216 urn:ioos:sensor:NOAA.NOS.CO-OPS:9753216:C1 -65.6311 18.3352 None
Yabucoa Harbor, PR 9754228 urn:ioos:sensor:NOAA.NOS.CO-OPS:9754228:C1 -65.8330 18.0551 None
San Juan, La Puntilla, San Juan Bay, PR 9755371 urn:ioos:sensor:NOAA.NOS.CO-OPS:9755371:C1 -66.1164 18.4592 None
Arecibo, PR 9757809 urn:ioos:sensor:NOAA.NOS.CO-OPS:9757809:C1 -66.7024 18.4805 None
Magueyes Island, PR 9759110 urn:ioos:sensor:NOAA.NOS.CO-OPS:9759110:C1 -67.0464 17.9701 None
Mayaguez, PR 9759394 urn:ioos:sensor:NOAA.NOS.CO-OPS:9759394:C1 -67.1625 18.2189 None
Barbuda, Antigua and Barbuda 9761115 urn:ioos:sensor:NOAA.NOS.CO-OPS:9761115:C1 -61.8206 17.5908 None

85 rows × 5 columns

For simplicity we will use only the stations that have both wind speed and sea surface height and reject those that have only one or the other.

common = set(ssh_table["station_code"]).intersection(wind_speed_table["station_code"])
ssh_obs, win_obs = [], []
for station in common:
    ssh_obs.extend([obs for obs in ssh if obs._metadata["station_code"] == station])
    win_obs.extend(
        [obs for obs in wind_speed if obs._metadata["station_code"] == station]
    )
index = pd.date_range(start=start, end=end, freq="15min")

# Re-index and rename series.
ssh_observations = []
for series in ssh_obs:
    _metadata = series._metadata
    series.index = series.index.tz_localize(None)
    obs = series.reindex(index=index, limit=1, method="nearest")
    obs._metadata = _metadata
    obs.name = _metadata["station_name"]
    ssh_observations.append(obs)

winds_observations = []
for series in win_obs:
    _metadata = series._metadata
    series.index = series.index.tz_localize(None)
    obs = series.reindex(index=index, limit=1, method="nearest")
    obs._metadata = _metadata
    obs.name = _metadata["station_name"]
    winds_observations.append(obs)

Let’s take a look at some stations to see if the data is OK. Below we have a station in Naples, FL along the Gulf of Mexico.

%matplotlib inline
import matplotlib.pyplot as plt

try:
    station = "8725110"

    w = [obs for obs in winds_observations if obs._metadata["station_code"] == station][
        0
    ]
    s = [obs for obs in ssh_observations if obs._metadata["station_code"] == station][0]

    fig, ax = plt.subplots(figsize=(17, 3.75))
    s["2017-9-10":].plot(ax=ax, label="Sea surface height (m)", color="#0000ff")
    ax1 = w["2017-9-10":].plot(
        ax=ax, label="Wind speed (m/s)", color="#9900cc", secondary_y=True
    )
    ax.set_title(w._metadata["station_name"])

    lines = ax.get_lines() + ax.right_ax.get_lines()
    ax.legend(lines, [l.get_label() for l in lines], loc="upper left")
    ax.axhline(0, color="black")

    ax.set_ylabel("Sea surface height (m)", color="#0000ff")
    ax.right_ax.set_ylabel("Wind speed (m/s)", color="#9900cc")

    ax1.annotate(
        "Eye of the hurricane",
        xy=(w["2017-9-10":].argmin().to_pydatetime(), w["2017-9-10":].min()),
        xytext=(5, 10),
        textcoords="offset points",
        arrowprops=dict(arrowstyle="simple", facecolor="crimson"),
    )

    ax.grid(True)
except Exception:
    print("Cannot find station {}".format(station))
/home/filipe/miniconda3/envs/IOOS/lib/python3.7/site-packages/ipykernel_launcher.py:23: FutureWarning: 
The current behaviour of 'Series.argmin' is deprecated, use 'idxmin'
instead.
The behavior of 'argmin' will be corrected to return the positional
minimum in the future. For now, use 'series.values.argmin' or
'np.argmin(np.array(values))' to get the position of the minimum
row.
../../../_images/2017-09-09-hurricane_irma_25_1.png

We can observe the sea level retreating around 10-Sep 9:00 and then a significant surge after 19:00. The lower winds at beginning of the surge is probably the eye of the hurricane.

For our interactive map we will use bokeh HTML plots instead of the usual raster matplotlib ones to enhance the user experience when exploring the graphs.

from bokeh.embed import file_html
from bokeh.models import HoverTool, LinearAxis, Range1d
from bokeh.plotting import figure
from bokeh.resources import CDN
from folium import IFrame

# Plot defaults.
tools = "pan,box_zoom,reset"
width, height = 750, 250


def make_plot(ssh, wind):
    p = figure(
        toolbar_location="above",
        x_axis_type="datetime",
        width=width,
        height=height,
        tools=tools,
        title=ssh.name,
    )

    p.yaxis.axis_label = "wind speed (m/s)"

    l0 = p.line(
        x=wind.index,
        y=wind.values,
        line_width=5,
        line_cap="round",
        line_join="round",
        legend="wind speed (m/s)",
        color="#9900cc",
        alpha=0.5,
    )

    p.extra_y_ranges = {}
    p.extra_y_ranges["y2"] = Range1d(start=-1, end=3.5)

    p.add_layout(LinearAxis(y_range_name="y2", axis_label="ssh (m)"), "right")

    l1 = p.line(
        x=ssh.index,
        y=ssh.values,
        line_width=5,
        line_cap="round",
        line_join="round",
        legend="ssh (m)",
        color="#0000ff",
        alpha=0.5,
        y_range_name="y2",
    )

    p.legend.location = "top_left"

    p.add_tools(
        HoverTool(tooltips=[("wind speed (m/s)", "@y"),], renderers=[l0],),
        HoverTool(tooltips=[("ssh (m)", "@y"),], renderers=[l1],),
    )
    return p


def make_marker(p, location, fname):
    html = file_html(p, CDN, fname)
    iframe = IFrame(html, width=width + 45, height=height + 80)

    popup = folium.Popup(iframe, max_width=2650)
    icon = folium.Icon(color="green", icon="stats")
    marker = folium.Marker(location=location, popup=popup, icon=icon)
    return marker

Here is the final result. Explore the map by clicking on the map features plotted!

import folium
from folium.plugins import Fullscreen, MarkerCluster
from ioos_tools.ioos import get_coordinates

lon = track.centroid.x
lat = track.centroid.y

m = folium.Map(location=[lat, lon], tiles="OpenStreetMap", zoom_start=4)

Fullscreen(position="topright", force_separate_button=True).add_to(m)

marker_cluster0 = MarkerCluster(name="Observations")
marker_cluster1 = MarkerCluster(name="Past predictions")
marker_cluster0.add_to(m)
marker_cluster1.add_to(m)


url = "http://oos.soest.hawaii.edu/thredds/wms/hioos/satellite/dhw_5km"
w0 = folium.WmsTileLayer(
    url,
    name="Sea Surface Temperature",
    fmt="image/png",
    layers="CRW_SST",
    attr="PacIOOS TDS",
    overlay=True,
    transparent=True,
)

w0.add_to(m)

url = "http://hfrnet.ucsd.edu/thredds/wms/HFRNet/USEGC/6km/hourly/RTV"
w1 = folium.WmsTileLayer(
    url,
    name="HF Radar",
    fmt="image/png",
    layers="surface_sea_water_velocity",
    attr="HFRNet",
    overlay=True,
    transparent=True,
)

w1.add_to(m)


def style_function(feature):
    return {
        "fillOpacity": 0,
        "color": "black",
        "stroke": 1,
        "weight": 0.5,
        "opacity": 0.2,
    }


# Latest cone prediction.
latest = cones[-1]
folium.GeoJson(
    data=latest.__geo_interface__,
    name="Cone prediction as of {}".format(latest["ADVDATE"].values[0]),
).add_to(m)

# Past cone predictions.
for cone in cones[:-1]:
    folium.GeoJson(data=cone.__geo_interface__, style_function=style_function,).add_to(
        marker_cluster1
    )

# Latest points prediction.
for k, row in pts.iterrows():
    date = row["FLDATELBL"]
    hclass = row["TCDVLP"]
    location = row["LAT"], row["LON"]
    popup = "{}<br>{}".format(date, hclass)
    folium.CircleMarker(
        location=location, radius=10, fill=True, color=colors[hclass], popup=popup,
    ).add_to(m)

# All the points along the track.
for point in points:
    date = point["FLDATELBL"]
    hclass = point["TCDVLP"]
    location = point["LAT"], point["LON"]
    popup = "{}<br>{}".format(date, hclass)
    folium.CircleMarker(
        location=location, radius=5, fill=True, color=colors[hclass], popup=popup,
    ).add_to(m)


# Observations.
for ssh, wind in zip(ssh_observations, winds_observations):
    fname = ssh._metadata["station_code"]
    location = ssh._metadata["lat"], ssh._metadata["lon"]
    p = make_plot(ssh, wind)
    marker = make_marker(p, location=location, fname=fname)
    marker.add_to(marker_cluster0)

folium.LayerControl().add_to(m)

p = folium.PolyLine(get_coordinates(bbox), color="#009933", weight=1, opacity=0.2)

p.add_to(m)
def embed_map(m):
    from IPython.display import HTML

    m.save("index.html")
    with open("index.html") as f:
        html = f.read()

    iframe = '<iframe srcdoc="{srcdoc}" style="width: 100%; height: 750px; border: none"></iframe>'
    srcdoc = html.replace('"', "&quot;")
    return HTML(iframe.format(srcdoc=srcdoc))


embed_map(m)
/home/filipe/miniconda3/envs/IOOS/lib/python3.7/site-packages/IPython/core/display.py:689: UserWarning: Consider using IPython.display.IFrame instead
  warnings.warn("Consider using IPython.display.IFrame instead")