HOTS Data

HOTS Data#

Chlorophyll data from Hawaii is gathered from the Hawaii Ocean Time-series (HOTS). More specifically, the Hawaii Ocean Time-series Data Organization & Graphical System (HOT-DOGS). https://hahana.soest.hawaii.edu/hot/hot-dogs/interface.html

From the Bottle data on HOT-DOGS, each individual station’s data is downloaded following these boundries:

  • Time: Jan 01 2000 -Dec 3 2023

  • Depth: 0-200m

  • variables: CTD salinity, CTD Temperature, Bottle Salinity, Fluorometric chlorophyll a, HPLC cholorophyll a

hot_bottle

Station Locations and file names (https://hahana.soest.hawaii.edu/hot/locations.html):

Download similar data from primary prod. with same time and depth, and chlorophyll a values.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import cartopy
import cartopy.crs as ccrs
import cartopy.feature as cfeature
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
import geopandas as gpd
from datetime import datetime
import os
from matplotlib import ticker
import datetime as dt
import plotly.express as px
import cmocean as cm
import cmocean.cm as cmo
import matplotlib.gridspec as gridspec
import time
import matplotlib.ticker as mticker
from PIL import Image
#def to convert degree minutes to degrees for lat and lon 
def dm_to_decimal_degrees(degrees, decimal_minutes):
    """Converts degrees and decimal minutes to decimal degrees.
    args:
        degrees: The whole number of degrees.
        decimal_minutes: The fractional part of the degree, in minutes.
    returns: The equivalent decimal degree value.
    """
    decimal_degrees = degrees + (decimal_minutes / 60)
    return decimal_degrees

#def to input all .nc files from folder 
def get_files(dir):
    file_list = []
    for root, _, files in os.walk(dir): #here, dir would be the path to the files
        for file in files:
            if file.endswith(".nc"):
                file_list.append(os.path.join(root, file)) #if file ends in .nc, append to list 
    return file_list
f_list1 =  'folderto\bottle_data' #folder holding ONLY the bottle data downloaded from HOTS 
f_list = get_files(f_list1) #list of all .nc files from folder

Next, go through each .nc file in the folder, remove any bad dates, and create datetime column. Then, depending on the station (based on value of stn column), append manually the lat and lon of the station, remove flagged chl values, and convert pressure to depth.

for file in f_list:
    print(file)
    dfs=pd.DataFrame() #initiate empty dataframe
    file2read = netcdf.NetCDFFile(file,'r') #read in the netcdf file
    varrs = file2read.variables #variables in netcdf file
    #remove the dataDesc, which is just a description of the data. if not removed, concatination can't occur
    del varrs["dataDesc"]
    for vars in varrs: #for every variable in the file
    #convert the byte to match specific laptop, and appened the variable column (0) into the dataframe
        dfs[vars]=pd.DataFrame(varrs[vars][:].byteswap().newbyteorder(), columns =['0']) 
        
    dfs=dfs[dfs['mdate']!=-9] #take out any negative datetimes i.e unrecorded data 
    dfs=dfs[dfs['mtime']!=-9]
 #datetime is formatted as mmddyy, and hhmmss, but leading 0s are removed so need to pad with 0s until 6 numbers long
    dfs['mtime'] = [f"{int(t):06d}" for t in dfs['mtime']] 
    dfs['mdate'] = [f"{int(t):06d}" for t in dfs['mdate']] 
    
    dfs['month'] = [int(t[:2]) for t in dfs['mdate']]
    dfs['day'] = [int(t[2:4]) for t in dfs['mdate']]
    dfs['year'] = [int(t[4:])+2000 for t in dfs['mdate']]
    dfs['hour'] = [int(t[:2]) for t in dfs['mtime']]
    dfs['minute'] = [int(t[2:4]) for t in dfs['mtime']]
    dfs['second'] = [int(t[4:]) for t in dfs['mtime']]
    dfs['datetime']= pd.to_datetime(dfs[['year', 'month', 'day', 'hour', 'minute', 'second']]) #datetime variable
    #drop unneeded temporal columns now that datetime is created
    columns_to_drop = ['mtime','mdate','month','day','year','hour','minute','second']
    dfs = dfs.drop(columns=columns_to_drop)
    dfs = dfs.reset_index(drop=True)
    
    #seperate data from each station into it's own dataframe, where the name of the dataframe equals the station name
    #lat and lon are not included in the file, so manually add them
    #finally, roughly convert pressure to depth (Depth(m)=Pressure(db) *1.019716)
    if dfs.stn[0]==1: #station 1 = station Kahe
        kahe=dfs.copy()
        kahe['lat']=dm_to_decimal_degrees(21, 20.6)
        kahe['lon']=dm_to_decimal_degrees(158, 16.4)*-1
        kahe=kahe[kahe['chl']!=-9] #remove -9 i.e. bad chl values
        kahe['station']='kahe'
        kahe['depth'] = kahe['press'] / 1.02
        kahe=kahe[['crn', 'depth', 'chl','hplc', 'datetime', 'lat', 'lon', 'station']]
        
    elif dfs.stn[0]==2: #station 2 = station Aloha
        aloha=dfs.copy()
        aloha['lat']=dm_to_decimal_degrees(22, 45.0)
        aloha['lon']=dm_to_decimal_degrees(158, 00.0)*-1
        #don't remove -9 from chl for aloha station because some bad chl entries still have hplc values. 
        aloha['station']='aloha'
        aloha['depth'] = aloha['press'] / 1.02
        aloha=aloha[['crn', 'depth', 'chl','hplc', 'datetime', 'lat', 'lon', 'station']]

    elif dfs.stn[0]==6: #station 6 = station kaena
        kaena=dfs.copy()
        kaena['lat']=dm_to_decimal_degrees(21, 50.8)
        kaena['lon']=dm_to_decimal_degrees(158, 7.9)*-1
        kaena=kaena[kaena['chl']!=-9] #remove -9 i.e. bad chl values
        kaena['station']='kaena'
        kaena['depth'] = kaena['press'] / 1.02
        kaena=kaena[['crn', 'depth', 'chl','hplc', 'datetime', 'lat', 'lon', 'station']]

    elif dfs.stn[0]==8: #station 8 = station Hale
        hale=dfs.copy()
        hale['lat']=dm_to_decimal_degrees(22, 27.5)
        hale['lon']=dm_to_decimal_degrees(158, 21.8)*-1
        hale=hale[hale['chl']!=-9] #remove -9 i.e. bad chl values
        hale['station']='hale'
        hale['depth'] = hale['press'] / 1.02
        hale=hale[['crn', 'depth', 'chl','hplc', 'datetime', 'lat', 'lon', 'station']]

    elif dfs.stn[0]==50: #station 50 = station Ors
        ors=dfs.copy()
        ors['lat']=dm_to_decimal_degrees(22, 45)
        ors['lon']=dm_to_decimal_degrees(157, 54.0)*-1
        ors=ors[ors['chl']!=-9] #remove -9 i.e. bad chl values
        ors['station']='ors'
        ors['depth'] = ors['press'] / 1.02
        ors=ors[['crn', 'depth', 'chl','hplc', 'datetime', 'lat', 'lon', 'station']]

    elif dfs.stn[0]==52: #station 52 = station Whots
        whots=dfs.copy()
        whots['lat']=dm_to_decimal_degrees(22, 40.208)
        whots['lon']=dm_to_decimal_degrees(157, 57.001)*-1
        whots=whots[whots['chl']!=-9] #remove -9 i.e. bad chl values
        whots['station']='whots'
        whots['depth'] = whots['press'] / 1.02
        whots=whots[['crn', 'depth', 'chl','hplc', 'datetime', 'lat', 'lon', 'station']]

    else:
        print(file) #if noe of the above, print the file to inspect
    file2read.close()  #close file

Once the Bottle data is done, complete the primary prod data.

hot_bottle

For primary production, create datetime column and initialize empty hplc column to match with the bottle data. Primary prod is only recorded at station aloha, so add that lat and lon.

ppro=pd.DataFrame()
file2read = netcdf.NetCDFFile('hd192096.nc','r')
varrs = file2read.variables #variables
del varrs["dataDesc"]
for vars in varrs:
    ppro[vars]=pd.DataFrame(varrs[vars][:].byteswap().newbyteorder(), columns =['0'])
ppro['stime'] = [f"{int(t):04d}" for t in ppro['stime']] #start time
ppro['etime'] = [f"{int(t):04d}" for t in ppro['etime']] #end time  
ppro['date'] = [f"{int(t):06d}" for t in ppro['date']]  #date
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[5], line 2
      1 ppro=pd.DataFrame()
----> 2 file2read = netcdf.NetCDFFile('hd192096.nc','r')
      3 varrs = file2read.variables #variables
      4 del varrs["dataDesc"]

NameError: name 'netcdf' is not defined
#arbitrarily deciding to use etime (end time) for datetime column
ppro['month'] = [int(t[2:4]) for t in ppro['date']]
ppro['day'] = [int(t[4:]) for t in ppro['date']]
ppro['year'] = [int(t[:2])+2000 for t in ppro['date']]
ppro['hour'] = [int(t[:2]) for t in ppro['etime']]
ppro['minute'] = [int(t[2:4]) for t in ppro['etime']]
ppro['datetime']= pd.to_datetime(ppro[['year', 'month', 'day', 'hour', 'minute']])
ppro=ppro[['crn', 'type', 'depth', 'chl', 'bsal','datetime']].reset_index(drop=True)  
ppro['station']='primary_prod'
ppro['hplc']=-9 #initiate empty hplc column so that when concatinated with stations it matches with other columns
#lat and lon to match station aloha 
ppro['lat']=dm_to_decimal_degrees(22, 45.0)
ppro['lon']=dm_to_decimal_degrees(158, 00.0)*-1
hots = pd.concat([kahe, aloha, kaena,hale,ors,whots,ppro]).reset_index(drop=True)

HPLC flag#

Some stations recorded HPLC data, under the column name ‘hplc’. If it’s not recorded, it’s entered as -9. So, if a column has hplc recorded, then flag as HPLC and convert to mg/L to match SeaBASS data and rename as chl_a.

#HPLC flag: if column hplc = -9, flag as 1 (bad), if there is a value, value, then convert to mg/L and flag as hplc=0
hots['HPLC'] = 1 #assume no HPLC
hots=hots.rename(columns={'hplc':'chl_a'})
hots.loc[hots['chl_a'] != -9, 'HPLC'] = 0 #if hplc column does not equal -9 change to 0
#Based on this linke, https://www.bco-dmo.org/dataset/3773, hplc is in nanograms/L, so need to convert
hots.loc[hots['chl_a'] != -9, 'chl_a'] =hots['chl_a']*0.001

triplicate flag#

Triplicate flags are flagged in the same way the SeaBASS data was, with detecting how many unique depths, datetime, lat, and lon entries there are and flagging if 3 samples were recorded at the exact same time/ hour.

counts_series = hots[['depth','datetime','lat','lon']].value_counts() #count how many unique cast,depth, datetime, lat, and lons there are
counts_df = counts_series.reset_index(name='freq_uniq')
hots = pd.merge(hots, counts_df, on=['depth','datetime','lat','lon'], how='left') #add frequency column to original dataframe4

#sometimes, triplicate specific times are recorded (ex: 3:00, 3:05, 3:10 ), so also check for unique datehour entries
hots['date_hour'] = hots['datetime'].dt.strftime('%Y-%m-%d %H')
counts_series = hots[['depth','date_hour','lat','lon']].value_counts() #count how many unique datehour, lat, and lons there are
counts_df = counts_series.reset_index(name='freq_hour')
hots = pd.merge(hots, counts_df, on=['depth','date_hour','lat','lon'], how='left') #add frequency column to original dataframe
hots['triplicate'] = 1 #assume bad unless otherwise said
hots.loc[hots['freq_uniq'] == 3, 'triplicate'] = 0 #if there was a unique datetime, lat, and lon that happened 3 times, triplicate
hots.loc[(hots['freq_uniq'] == 1) &(hots['freq_hour'] == 3), 'triplicate'] = 0 #if 1 unique datetime but 3 unique date hours, assume triplicate
hots=hots.loc[hots['depth']<=150].reset_index(drop=True) #reduce to upper 150 meteres for algorithm development

hots=hots[['datetime', 'lat', 'lon','depth', 'chl', 'chl_a', 'station', 'HPLC', 'triplicate']]
hots['experiment']='HOTS'
hots['source']='HOT-DOGS'
hots['investigators']= 'Angelicque E. White'
hots['affiliations']="University of Hawai'i at Mānoa"
hots['url']='https://hahana.soest.hawaii.edu/hot/hot-dogs/interface.html'
hots.replace(-9, np.nan, inplace=True)
hots = hots.dropna(subset=['chl', 'chl_a'], how='all')

Done with Hots data!!

Data obtained via the Hawaii Ocean Time-series HOT-DOGS application; University of Hawai’i at Mānoa. National Science Foundation Award # 2241005

Plots#

HOTS = pd.read_excel(r'C:\Users\gianna.milton\Documents\Python\Coastal_chl_final\hots_chl_qc.xlsx')
year_test=HOTS.copy()
year_test['datetime'] = pd.to_datetime(year_test['datetime'])
year_test['year'] = year_test['datetime'].dt.year
grouped = year_test.groupby(['year']).size().reset_index(name='DataPoints')

# Create bar chart
fig = px.bar(grouped,x='year', y='DataPoints', title='Yearly distribution all data',
             labels={'year': 'Year', 'DataPoints': 'Number of Data Points', 'metadata': 'Metadata'},)
fig.update_xaxes(range=[1999,2027])
fig.update_layout(barmode='stack')  # ensures stacking
fig.show()
_images/f822ad1eb48617f14f234a0ef4a308866b1d0b7db4884d577358082caa267647.png
year_test['log_chl']=np.log10(year_test['chl'])
year_test['log_chl_a']=np.log10(year_test['chl_a'])

fig=plt.figure(figsize=(15, 10))
axs1=fig.add_subplot(1,2,1,projection= cartopy.crs.PlateCarree())
axs1.add_feature(cfeature.LAND)
axs1.add_feature(cfeature.OCEAN)
axs1.add_feature(cfeature.COASTLINE)
axs1.add_feature(cfeature.BORDERS)
axs1.add_feature(cfeature.STATES, linestyle='-', linewidth=0.5)
im=axs1.scatter(year_test.lon,year_test.lat,c=year_test.log_chl,cmap=cmo.algae,s=30,vmin=-1, vmax=1, edgecolors='black', linewidths=0.5)
axs1.set_title('HOTS Chlorophyll data')
axs1.set_xlim(min(year_test.lon)-3,max(year_test.lon)+3)
axs1.set_ylim(min(year_test.lat)-1,max(year_test.lat)+1)
cb=fig.colorbar(im,ax=axs1,orientation='horizontal', pad=0.05)
cb.set_label('log chl',fontsize=12)
gl=axs1.gridlines(linewidth=0.2,color='grey',alpha=0.7,linestyle='-', draw_labels=True, x_inline= False,y_inline=False)
gl.xformatter=LONGITUDE_FORMATTER
gl.yformatter=LATITUDE_FORMATTER
gl.top_labels = False    # Disable top labels
gl.right_labels = False  # Disable right labels

axs2=fig.add_subplot(1,2,2,projection= cartopy.crs.PlateCarree())
axs2.add_feature(cfeature.LAND)
axs2.add_feature(cfeature.OCEAN)
axs2.add_feature(cfeature.COASTLINE)
axs2.add_feature(cfeature.BORDERS)
axs2.add_feature(cfeature.STATES, linestyle='-', linewidth=0.5)
im=axs2.scatter(year_test.lon,year_test.lat,c=year_test.log_chl_a,cmap=cmo.algae,s=30,vmin=-1, vmax=1, edgecolors='black', linewidths=0.5)
axs2.set_title('HOTS HPLC data')
axs2.set_xlim(min(year_test.lon)-3,max(year_test.lon)+3)
axs2.set_ylim(min(year_test.lat)-1,max(year_test.lat)+1)
cb=fig.colorbar(im,ax=axs2,orientation='horizontal', pad=0.05)
cb.set_label('log chl_a',fontsize=12)
gl=axs2.gridlines(linewidth=0.2,color='grey',alpha=0.7,linestyle='-', draw_labels=True, x_inline= False,y_inline=False)
gl.xformatter=LONGITUDE_FORMATTER
gl.yformatter=LATITUDE_FORMATTER
gl.top_labels = False    # Disable top labels
gl.right_labels = False  # Disable right labels
_images/8ce7fcb056d194843369c0828c722eddb698b0dd2e444fa9094cbac8a8a686a2.png
from matplotlib.colors import LogNorm # Important for high-variance data
fig = plt.figure(figsize=(15, 9))
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree())
ax.add_feature(cfeature.LAND)
ax.add_feature(cfeature.OCEAN)
ax.add_feature(cfeature.COASTLINE)
ax.add_feature(cfeature.BORDERS)
ax.add_feature(cfeature.STATES)
hb = ax.hexbin(year_test.lon, year_test.lat, gridsize=(5,5), cmap='inferno_r', mincnt=1, transform=ccrs.PlateCarree(),norm=LogNorm()) 
cb = plt.colorbar(hb, ax=ax, orientation='vertical', pad=0.02, shrink=0.8)
cb.set_label('Number of Datapoints', fontsize=14)
gl=ax.gridlines(linewidth=0.2,color='grey',alpha=0.7,linestyle='-', draw_labels=True, x_inline= False,y_inline=False)
gl.xformatter=LONGITUDE_FORMATTER
gl.yformatter=LATITUDE_FORMATTER
gl.top_labels = False    # Disable top labels
gl.right_labels = False  # Disable right labels
ax.set_extent([-161, -155, 20, 24], crs=ccrs.PlateCarree())

ax.set_title('Spatial Data Density', fontsize=18, fontweight='bold')

plt.show()
_images/b539b1cffe791c016d999128988dbf730cadf00d50145a452b7462bec4125028.png