WOD data#

All chlorophyll data from World Ocean Databse (WOD) was compliled from WOD select (https://www.ncei.noaa.gov/access/world-ocean-database-select/dbsearch.html)

WOD was gathered in 3 rounds. The first round had the following entries:

  • lon: -127 to -52

  • lat: 76 to 8

  • dates: 2000-01-01 to 2024- 04-28

  • dataset: OSD, MRB, SUR

  • variable: total chlorophyll

The second round had the following entries:

  • lon: -162 to -47

  • lat: 77 to 15

  • dates: 2000-01-01 to 2025- 04-01

  • dataset: OSD, CTD,PFL

  • variable: total Chlorophyll

Finally, to ensure ALL ECOMON data is gathered, from WOD select use project query and only gather project with code 637 (ECOMON)

A lot of WOD data relies on code IDs for their metadata. For example, they don’t save the project name but rather assign the project a code number and put that in the column. Use the following files for what each code means: https://www.ncei.noaa.gov/access/world-ocean-database/wod-codes.html

Bear in mind this wod-codes is not completely comprehensive as some entries are missing metadata 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

The raw data is organized in csv files into cast blocks, where the rows with ‘END VARIABLE SECTION’ designate the end of that cast block. The def loop below identifies the indexes of each block and processes them individually, including appending the variables into one single dataframe.

Here’s an example of the raw csv file:

wod_cast

#start def loop for reading and organizing raw WOD files
def wod_df(excel_file):
    
    '''
    arg: path to the raw excelfile 
    return: dataframe with unique columns for each variable/ metadata 
    '''
    WOD_raw = pd.read_excel(excel_file, header=None) #load in that excel file 
    WOD_raw[0] = WOD_raw[0].str.lower() #since there are some inconsistancies with column cases, put everything as lower case
    #identify cast_blocks (rows between '#' and 'END OF VARIABLES SECTION')
    cast_blocks = []
    start_idx = None #to track the start of the index
    
    #first, identify and record the index positions of the cast blocks, i.e. the start and end of each cast
    for idx, row in WOD_raw.iterrows(): #for every row
        first_cell = str(row[0]) if pd.notna(row[0]) else "" #the first cell of the row (as long as it exists)
        if first_cell.startswith("#") and "end of variables section" not in first_cell: #if it's the start of the cast and NOT the end
            if start_idx is not None:
                cast_blocks.append((start_idx, idx)) #if the first cell is #, record the start indx
            start_idx = idx #start of the new cast
        
        elif "end of variables section" in first_cell: #if it's the end of a cast, record the index
            if start_idx is not None:
                cast_blocks.append((start_idx, idx)) #append this index as the end
                start_idx = None
                
                
    #now that we have the indeces to distinguish each cast block, process the casts individually 
    all_data = []
    for start, end in cast_blocks: #for each pair of index vales in cast_block
        cruise_df = WOD_raw.iloc[start:end+1].reset_index(drop=True) #loacte those indexes of that block and make a dataframe
        metadata = {}
        variable_data_started = False #flag to indicate if inside the VARIABLES section
        variable_data = []
        for _, row in cruise_df.iterrows(): #for each row in the block of the cast
            row_values = row.dropna().astype(str).tolist() #convert to string
            if not row_values: #skip all empty rows 
                continue
            if "variables" in row_values[0]: #if the start of the varibale section, turn to true
                variable_data_started = True
                continue
            if "end of variables section" in row_values[0]: #if at the end, break and go back to for loop
                break
    
    
            if not variable_data_started: #if we're in the metadata section
                key = row_values[0].strip() #seperate the row into sections
                if len(row_values) > 1: #if we are in the metadata section
                    value = row_values[1].strip() #take from column 2
                    metadata[key] = value #save as metadata
                    #metadata.columns.str.lower()

            else: #if in variable row
                try:
                    if row_values[0].isdigit() and len(row_values) >= 6: 
                        depth = float(row_values[1]) #append depth
                        chl = float(row_values[4]) # append chlorophyll
                        variable_data.append((depth, chl))
                        #variable_data.columns.str.lower()
                except:
                    continue
    
        #append metadata and variables
        for depth, chl in variable_data: #for this row of variables, rename, then append to the dataframe
            row = metadata.copy()
            row["depth (m)"] = depth
            row["chlorophyll (ug/l)"] = chl
            all_data.append(row)
   
    df = pd.DataFrame(all_data)
    #create datetime
    df["datetime"] = pd.to_datetime(df[["year", "month", "day"]]) + df["time"].apply(lambda t: timedelta(hours=float(t)) if pd.notnull(t) else pd.NaT)
    return df

File 1/5#

wod_1 = wod_df(r'C:\Users\gianna.milton\Documents\Python\WOD\ocldb1748619702.620293.OSD.csv\ocldb1748619702.620293.OSD.xlsx')
wod_1=wod_1[wod_1['project'] != '33'] #take out CalCOFI values
wod_1=wod_1[wod_1['project'] != '301'] #take out HOTS values 

#rename columns 
wod_1=wod_1.rename(columns={'depth (m)':'depth','longitude':'lon','latitude':'lat','chlorophyll (ug/l)':'chl'})
#for this project, only want top 150 meters
wod_1=wod_1.loc[wod_1['depth']<=150].reset_index(drop=True) 
wod_1.lon= wod_1.lon.astype(float)
wod_1.lat= wod_1.lat.astype(float)
wod_1.chl=wod_1.chl.astype(float)
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[3], line 1
----> 1 wod_1 = wod_df(r'C:\Users\gianna.milton\Documents\Python\WOD\ocldb1748619702.620293.OSD.csv\ocldb1748619702.620293.OSD.xlsx')
      2 wod_1=wod_1[wod_1['project'] != '33'] #take out CalCOFI values
      3 wod_1=wod_1[wod_1['project'] != '301'] #take out HOTS values 

Cell In[2], line 31, in wod_df(excel_file)
     29 all_data = []
     30 for start, end in cast_blocks: #for each pair of index vales in cast_block
---> 31     cruise_df = WOD_raw.iloc[start:end+1].reset_index(drop=True) #loacte those indexes of that block and make a dataframe
     32     metadata = {}
     33     variable_data_started = False #flag to indicate if inside the VARIABLES section

File ~\AppData\Local\anaconda3\Lib\site-packages\pandas\core\frame.py:6417, in DataFrame.reset_index(self, level, drop, inplace, col_level, col_fill, allow_duplicates, names)
   6415     new_obj = self
   6416 else:
-> 6417     new_obj = self.copy(deep=None)
   6418 if allow_duplicates is not lib.no_default:
   6419     allow_duplicates = validate_bool_kwarg(allow_duplicates, "allow_duplicates")

File ~\AppData\Local\anaconda3\Lib\site-packages\pandas\core\generic.py:6811, in NDFrame.copy(self, deep)
   6662 @final
   6663 def copy(self, deep: bool_t | None = True) -> Self:
   6664     """
   6665     Make a copy of this object's indices and data.
   6666 
   (...)
   6809     dtype: int64
   6810     """
-> 6811     data = self._mgr.copy(deep=deep)
   6812     self._clear_item_cache()
   6813     return self._constructor_from_mgr(data, axes=data.axes).__finalize__(
   6814         self, method="copy"
   6815     )

File ~\AppData\Local\anaconda3\Lib\site-packages\pandas\core\internals\managers.py:604, in BaseBlockManager.copy(self, deep)
    601         res._blklocs = self._blklocs.copy()
    603 if deep:
--> 604     res._consolidate_inplace()
    605 return res

File ~\AppData\Local\anaconda3\Lib\site-packages\pandas\core\internals\managers.py:1788, in BlockManager._consolidate_inplace(self)
   1782 def _consolidate_inplace(self) -> None:
   1783     # In general, _consolidate_inplace should only be called via
   1784     #  DataFrame._consolidate_inplace, otherwise we will fail to invalidate
   1785     #  the DataFrame's _item_cache. The exception is for newly-created
   1786     #  BlockManager objects not yet attached to a DataFrame.
   1787     if not self.is_consolidated():
-> 1788         self.blocks = _consolidate(self.blocks)
   1789         self._is_consolidated = True
   1790         self._known_consolidated = True

File ~\AppData\Local\anaconda3\Lib\site-packages\pandas\core\internals\managers.py:2269, in _consolidate(blocks)
   2267 new_blocks: list[Block] = []
   2268 for (_can_consolidate, dtype), group_blocks in grouper:
-> 2269     merged_blocks, _ = _merge_blocks(
   2270         list(group_blocks), dtype=dtype, can_consolidate=_can_consolidate
   2271     )
   2272     new_blocks = extend_blocks(merged_blocks, new_blocks)
   2273 return tuple(new_blocks)

File ~\AppData\Local\anaconda3\Lib\site-packages\pandas\core\internals\managers.py:2285, in _merge_blocks(blocks, dtype, can_consolidate)
   2280     return blocks, False
   2282 if can_consolidate:
   2283     # TODO: optimization potential in case all mgrs contain slices and
   2284     # combination of those slices is a slice, too.
-> 2285     new_mgr_locs = np.concatenate([b.mgr_locs.as_array for b in blocks])
   2287     new_values: ArrayLike
   2289     if isinstance(blocks[0].dtype, np.dtype):
   2290         # error: List comprehension has incompatible type List[Union[ndarray,
   2291         # ExtensionArray]]; expected List[Union[complex, generic,
   2292         # Sequence[Union[int, float, complex, str, bytes, generic]],
   2293         # Sequence[Sequence[Any]], SupportsArray]]

KeyboardInterrupt: 

HPLC flags#

In order to flag for HPLC, seach up each row that has a project name and research if they used HPLC. If no project is included, just assume no HPLC was used

For file 1/5, the following projects were researched:

#Begin flagging
wod_1['HPLC']=1 #assume all points are not hplc
wod_1.loc[wod_1['project'] == '412', 'HPLC'] = 0 
wod_1.loc[wod_1['project'] == '311', 'HPLC'] = 0
wod_1=wod_1[[ 'datetime','lat', 'lon','chl','depth','cast','originators cruise id', 'project','institute','instrument', 'investigator', 'HPLC','accession number']]

Triplicate flags#

Triplicates are found the same way as SeaBASS; by counting the number of unique cast, depths, datetime/date hours, lat, and lon and flagging where there are exactly 3 unique entries.

# triplicate flag
counts_series = wod_1[['cast','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')
wod_1 = pd.merge(wod_1, counts_df, on=['cast','depth','datetime','lat','lon'], how='left') #add frequency column to original dataframe

#sometimes, triplicate specific times are recorded (ex: 3:00, 3:05, 3:10 ), so also check for unique datehour entries
wod_1['date_hour'] = wod_1['datetime'].dt.strftime('%Y-%m-%d %H')
counts_series = wod_1[['cast','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')
wod_1 = pd.merge(wod_1, counts_df, on=['cast','depth','date_hour','lat','lon'], how='left') #add frequency column to original dataframe

wod_1['triplicate'] = 1 #assume bad unless otherwise said
wod_1.loc[wod_1['freq_uniq'] == 3, 'triplicate'] = 0 #if there was a unique datetime, lat, and lon that happened 3 times, triplicate
wod_1.loc[(wod_1['freq_uniq'] == 1) &(wod_1['freq_hour'] == 3), 'triplicate'] =0 #if 1 unique datetime and 3 unique date_hours, assume triplicate

File 2/5#

wod_2 = wod_df(r'C:\Users\gianna.milton\Documents\Python\WOD\WOD_round2_raw\ocldb1761586645.2754592.OSD.xlsx')
wod_2=wod_2[wod_2['project'] != '33'] #take out CalCOFI since appended elsewhere
wod_2=wod_2[wod_2['project'] != '301'] #take out HOTS since appended elsewhere
wod_2=wod_2.rename(columns={'depth (m)':'depth','longitude':'lon','latitude':'lat','chlorophyll (ug/l)':'chl'})
wod_2=wod_2.loc[wod_2['depth']<=150].reset_index(drop=True) 
wod_2.lon= wod_2.lon.astype(float)
wod_2.lat= wod_2.lat.astype(float)
wod_2.chl=wod_2.chl.astype(float)

HPLC flags per project.

wod_2['HPLC']=1 #assume all points are not hplc
wod_2.loc[wod_2['project'] == '412', 'HPLC'] = 0
wod_2=wod_2[[ 'datetime','lat', 'lon','chl','depth','cast','originators cruise id', 'project','institute','instrument', 'investigator', 'HPLC','accession number']]

#triplicate flag
counts_series = wod_2[['cast','depth','datetime','lat','lon']].value_counts() 
counts_df = counts_series.reset_index(name='freq_uniq')
wod_2 = pd.merge(wod_2, counts_df, on=['cast','depth','datetime','lat','lon'], how='left') 
wod_2['date_hour'] = wod_2['datetime'].dt.strftime('%Y-%m-%d %H')
counts_series = wod_2[['cast','depth','date_hour','lat','lon']].value_counts() 
counts_df = counts_series.reset_index(name='freq_hour')
wod_2 = pd.merge(wod_2, counts_df, on=['cast','depth','date_hour','lat','lon'], how='left') 

wod_2['triplicate'] = 1 #assume bad unless otherwise said
wod_2.loc[wod_2['freq_uniq'] == 3, 'triplicate'] = 0 #if there was a unique datetime, lat, and lon that happened 3 times, triplicate
wod_2.loc[(wod_2['freq_uniq'] == 1) &(wod_2['freq_hour'] == 3), 'triplicate'] = 0 #if 1 unique datetime recorded and only 3 for datehour, assume triplicate 

File 3/5#

wod_3 = wod_df(r'C:\Users\gianna.milton\Documents\Python\WOD\WOD_round2_raw\ocldb1761586645.2754592.CTD.xlsx')
wod_3=wod_3[wod_3['project'] != '301'] #take out HOTS 
wod_3=wod_3[wod_3['project'] != '637'] #take out ECOMON since running it later

HPLC flags per project

  • 121 ->SOUTHEAST AREA MONITORING AND ASSESSMENT PROGRAM (SEAMAP) https://www.gsmfc.org/seamap-gomrs, no hplc

  • 597 ->HYPOXIA STUDIES IN THE NORTHERN GULF OF MEXICO, no hplc

wod_3['HPLC']=1

wod_3=wod_3.rename(columns={'depth (m)':'depth','longitude':'lon','latitude':'lat','chlorophyll (ug/l)':'chl'})
wod_3=wod_3.loc[wod_3['depth']<=150].reset_index(drop=True) 
wod_3.lon= wod_3.lon.astype(float)
wod_3.lat= wod_3.lat.astype(float)
wod_3.chl=wod_3.chl.astype(float)

wod_3=wod_3[[ 'datetime','lat', 'lon','chl','depth','cast','originators cruise id', 'project','institute','instrument', 'investigator', 'HPLC','accession number']]
counts_series = wod_3[['cast','depth','datetime','lat','lon']].value_counts()
counts_df = counts_series.reset_index(name='freq_uniq')
wod_3 = pd.merge(wod_3, counts_df, on=['cast','depth','datetime','lat','lon'], how='left') 
wod_3['date_hour'] = wod_3['datetime'].dt.strftime('%Y-%m-%d %H')
counts_series = wod_3[['cast','depth','date_hour','lat','lon']].value_counts() 
counts_df = counts_series.reset_index(name='freq_hour')
wod_3 = pd.merge(wod_3, counts_df, on=['cast','depth','date_hour','lat','lon'], how='left')

wod_3['triplicate'] = 1 #assume bad unless otherwise said
wod_3.loc[wod_3['freq_uniq'] == 3, 'triplicate'] = 0 
wod_3.loc[(wod_3['freq_uniq'] == 1) &(wod_3['freq_hour'] == 3), 'triplicate'] =0 

File 4/5#

wod_4 = wod_df(r'C:\Users\gianna.milton\Documents\Python\WOD\WOD_round2_raw\ocldb1761586645.2754592.CTD2.xlsx')
wod_4=wod_4[wod_4['project'] != '637'] #take out ECOMON

This file has the same projects as the last one, so no HPLC.

wod_4['HPLC']=1 #no hplc here i think

wod_4=wod_4.rename(columns={'depth (m)':'depth','longitude':'lon','latitude':'lat','chlorophyll (ug/l)':'chl'})
wod_4=wod_4.loc[wod_4['depth']<=150].reset_index(drop=True) 
wod_4.lon= wod_4.lon.astype(float)
wod_4.lat= wod_4.lat.astype(float)
wod_4.chl=wod_4.chl.astype(float)

wod_4=wod_4[[ 'datetime','lat', 'lon','chl','depth','cast','originators cruise id', 'project','institute','instrument', 'investigator', 'HPLC','accession number']]
counts_series = wod_4[['cast','depth','datetime','lat','lon']].value_counts()
counts_df = counts_series.reset_index(name='freq_uniq')
wod_4 = pd.merge(wod_4, counts_df, on=['cast','depth','datetime','lat','lon'], how='left')
wod_4['date_hour'] = wod_4['datetime'].dt.strftime('%Y-%m-%d %H')
counts_series = wod_4[['cast','depth','date_hour','lat','lon']].value_counts() 
counts_df = counts_series.reset_index(name='freq_hour')
wod_4 = pd.merge(wod_4, counts_df, on=['cast','depth','date_hour','lat','lon'], how='left')

wod_4['triplicate'] = 1 #assume bad unless otherwise said
wod_4.loc[wod_4['freq_uniq'] == 3, 'triplicate'] = 0 
wod_4.loc[(wod_4['freq_uniq'] == 1) &(wod_4['freq_hour'] == 3), 'triplicate'] = 0 

File 5/5 (ECOMON)#

wod_ecomon = wod_df(r'C:\Users\gianna.milton\Documents\Python\WOD\ocldb1761249916.1960703.CTD.csv\ocldb1761249916.1960703.CTD.xlsx')
wod_ecomon['HPLC']=1 
wod_ecomon=wod_ecomon.rename(columns={'depth (m)':'depth','longitude':'lon','latitude':'lat','chlorophyll (ug/l)':'chl'})
wod_ecomon=wod_ecomon.loc[wod_ecomon['depth']<=150].reset_index(drop=True) 
wod_ecomon.lon= wod_ecomon.lon.astype(float)
wod_ecomon.lat= wod_ecomon.lat.astype(float)
wod_ecomon.chl=wod_ecomon.chl.astype(float)

wod_ecomon=wod_ecomon[[ 'datetime','lat', 'lon','chl','depth','cast','originators cruise id', 'project','institute', 'HPLC','accession number']]
counts_series = wod_ecomon[['cast','depth','datetime','lat','lon']].value_counts()
counts_df = counts_series.reset_index(name='freq_uniq')
wod_ecomon = pd.merge(wod_ecomon, counts_df, on=['cast','depth','datetime','lat','lon'], how='left')
wod_ecomon['date_hour'] = wod_ecomon['datetime'].dt.strftime('%Y-%m-%d %H')
counts_series = wod_ecomon[['cast','depth','date_hour','lat','lon']].value_counts() 
counts_df = counts_series.reset_index(name='freq_hour')
wod_ecomon = pd.merge(wod_ecomon, counts_df, on=['cast','depth','date_hour','lat','lon'], how='left')

wod_ecomon['triplicate'] = 1 
wod_ecomon.loc[wod_ecomon['freq_uniq'] == 3, 'triplicate'] = 0 
wod_ecomon.loc[(wod_ecomon['freq_uniq'] == 1) &(wod_ecomon['freq_hour'] == 3), 'triplicate'] = 0

Final concatination#

Concat all the files into 1 dataframe and organize to better match with the SeaBASS data. Then, subset to inside North American shapefile

dfs = [wod_1,wod_2,wod_3,wod_4,wod_ecomon]
wod_all = pd.concat(dfs)
wod_all=wod_all[['datetime', 'lat', 'lon', 'chl', 'depth', 'cast','originators cruise id', 'project', 'institute', 'instrument',
       'investigator', 'HPLC', 'triplicate','accession number']]
wod_all = wod_all.rename(columns={'originators cruise id':'cruise','project':'experiment','institute':'affiliations','investigator':'investigators'})

Since WOD is a repository with no way of distinguishing between extacted and in vivo, add a data_type_flag to help distinguishe between high resolution sampling methods

wod_all['t_flag']=0 #initialize temporal resolution flag, 0=good, 1= bad (less than 1hour),2=flag (time is 0 i.e repeated)
wod_all['diff_time'] = 0 #column to populate with datatypes as values for organization
wod_all['d_flag'] = 0 #initialize depth flag, 0=good, 1=bad (less than 5m), 2=flag
wod_all['decision'] = 2 #ultimate decision flag inidcating whether to keep or toss data point (0=good, 1=bad,2=flag)

# time 
wod_all['diff_time']= wod_all['datetime'].diff()
wod_all.t_flag=np.where(wod_all['diff_time']< pd.to_timedelta('10 minutes'), 1, wod_all.t_flag) #if delta t is less than 1 hour, flag as bad
wod_all.t_flag=np.where(wod_all['diff_time']== pd.to_timedelta(0), 2, wod_all.t_flag) #if 0, then just a repeat so not necessarily bad

#depth

depth_diff =abs(wod_all.depth.diff())#calculate absolute change in depth
wod_all.loc[wod_all[depth_diff<1].index,'d_flag']=1 #if the change in depth is not large enough
wod_all.loc[wod_all[depth_diff==0].index,'d_flag']=2 #if the change in depth doesn't move, set as 2, diff_time and num_s will take care of it

wod_all.decision[(wod_all['t_flag'] ==0) & (wod_all['d_flag']==0)] = 0 #if both good, then good
wod_all.decision[(wod_all['t_flag'] ==0) & (wod_all['d_flag']==1)] = 1 #if everything else is good but the depth is too short, flag as nad
wod_all.decision[(wod_all['t_flag'] ==0) & (wod_all['d_flag']==2)] = 0 #if everything else is good and depth repeats, good
wod_all.decision[(wod_all['t_flag'] ==1) & (wod_all['d_flag']==0)] = 1 #IF TIME IS EVER BAD then the whole thing is bad
wod_all.decision[(wod_all['t_flag'] ==1) & (wod_all['d_flag']==1)] = 1
wod_all.decision[(wod_all['t_flag'] ==1) & (wod_all['d_flag']==2)] = 1
wod_all.decision[(wod_all['t_flag'] ==2) & (wod_all['d_flag']==0)] = 0
wod_all.decision[(wod_all['t_flag'] ==2) & (wod_all['d_flag']==1)] = 1
wod_all.decision[(wod_all['t_flag'] ==2) & (wod_all['d_flag']==2)] = 1
#remove depth flagged data sinve it's the most idicative in vivo flag
wod_all=wod_all[wod_all['d_flag']!=1]

wod_all=wod_all[['datetime', 'lat', 'lon', 'chl', 'depth', 'cast','cruise', 'experiment', 'affiliations', 'instrument',
       'investigators', 'HPLC', 'triplicate','accession number', 'decision']]
shp = gpd.read_file(r'C:\Users\gianna.milton\Documents\Python\Shapefiles\combined_coastline.shp')
gdf = gpd.GeoDataFrame(wod_all, geometry=gpd.points_from_xy(wod_all.lon, wod_all.lat), crs="EPSG:4269")
gdf = gdf.to_crs(shp.crs)
wod_all = gpd.sjoin(gdf, shp, how="inner", predicate="within")
columns_to_drop = ['geometry', 'index_right', 'merge_id']
wod_all = wod_all.drop(columns=columns_to_drop)
wod_all= wod_all.reset_index(drop=True)

Done with WOD chlorophyll data!!!

Mishonov A.V., T. P. Boyer, O. K. Baranova, C. N. Bouchard, S. Cross, H. E. Garcia, R. A. Locarnini, C. R. Paver, J. R. Reagan, Z. Wang, D. Seidov, A. I. Grodsky, J. G. Beauchamp, (2024): World Ocean Database 2023. C. Bouchard, Technical Ed., NOAA Atlas NESDIS 97, 206 pp., doi.org/10.25923/z885-h264

Plots#

wod = pd.read_excel(r'C:\Users\gianna.milton\Documents\Python\Coastal_chl_final\wod_chl_na.xlsx')
year_test=wod.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/06ffc66800110b6bc9c1ce72762452dcbeb3f99368e75945632557ef39fd1103.png
from matplotlib.colors import LogNorm # Important for high-variance data
fig = plt.figure(figsize=(15, 10))
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=50, 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_xlim(min(year_test.lon)-2,max(year_test.lon)+2)
ax.set_ylim(min(year_test.lat)-2,max(year_test.lat)+2)

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

plt.show()
_images/1da94eca9497109ce326cb7e0e6d28622e6ff445ee802303a0f764c25faa9d64.png