SeaBASS Data

Contents

SeaBASS Data#

all Concentration of chromophoric dissolved organic matter (CDOM) data on seabass was bulk downloaded from https://seabass.gsfc.nasa.gov/search#bio

The following boundries were used to gather CDOM and suspended data from seabass:

  • dates: 2000-01-01 -> 2025-12-17

  • lat: 74 to 17

  • lon: -171 to -52

  • products: a, DC, PC, SPM

get_files and get_folder_name are both used to gather all folders names in the requested files and to gather all .sb files within those folders

Please refer to the seabass_chl page for a deeper understanding on the seabass file structure.

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
def get_files(dir):
    file_list = []
    for root, _, files in os.walk(dir): #here, dir would be the path to the requested_files
        for file in files:
            if file.endswith(".sb"):
                file_list.append(os.path.join(root, file))
    return file_list

def get_folder_names(folder_path):
  """
  Returns a list of folder names in the given directory.
  """
  folder_names = []
  for item in os.listdir(folder_path):
    item_path = os.path.join(folder_path, item)
    if os.path.isdir(item_path):
      folder_names.append(item)
  return folder_names

SeaBASS compiles raw data by including all variables recorded along with the ones asked for. This can result in sometimes thousands of column with variables we don’t want. So subset the columns.

selected_columns =['lat','lon','year','month','day','hour','minute','second','time','date','datetime' ,'ag', 'abs_ag', 'cdomf', 'cdmf', 'cdmf_rfu',
                   'pim', 'pic', 'spm', 'pom', 'turbidity', 'cdom','depth','station','wavelength'] #detect any column related to cdom

path_to_folder = r'C:\Users\gianna.milton\Documents\Python\CDOM\SB_products_all2\requested_files_5\requested_files'
all_folders = get_folder_names(path_to_folder)

The following three cells follow a very similar structure as to Seabass chlorophyll data; sb.files are turned into structured dataframes, all empty rows without the variables we want are removed, and lat, lon, and datetime are appended.

for folders in range(len(all_folders)):
    #print(folders)
    f_list1 =  path_to_folder +'\\'+str(all_folders[folders])
    f_list = get_files(f_list1)
    print(str(all_folders[folders]))
    dfs = []  # list to collect all processed DataFrames
    for file in f_list:
        data1 = sb.readSB(filename=file, no_warn=True)
        data2 = data1.data
        df = pd.DataFrame.from_dict(data2, orient='index').T
        dt = None  # initialize datetime variable
        # Generate datetime column if possible
        if all(col in df.columns for col in ['year', 'month', 'day', 'hour', 'minute', 'second']):
            dt = pd.to_datetime(df[['year', 'month', 'day', 'hour', 'minute', 'second']])
        elif all(col in df.columns for col in ['year', 'month', 'day', 'hour', 'minute']):
            dt = pd.to_datetime(df[['year', 'month', 'day', 'hour', 'minute']])
        elif all(col in df.columns for col in ['year', 'month', 'day', 'time']):
            df['hour'] = df['time'].astype(str).str[:-6].astype(int)
            df['minute'] = df['time'].astype(str).str[-5:-3].astype(int)
            df['second'] = df['time'].astype(str).str[-2:].astype(int)
            dt = pd.to_datetime(df[['year', 'month', 'day', 'hour', 'minute', 'second']])
        elif all(col in df.columns for col in ['date', 'time']):
            df['year'] = df['date'].astype(str).str[:4].astype(int)
            df['month'] = df['date'].astype(str).str[4:6].astype(int)
            df['day'] = df['date'].astype(str).str[6:8].astype(int)
            time_strs = df['time'].astype(str) #some files have time as HH:mm, some as HH:mm:ss, so this accounts for both
            if time_strs.str.contains(":").all() and time_strs.str.count(":").eq(2).all():  #determine format based on string length or presence of ":"
                df['hour'] = time_strs.str.split(":").str[0].astype(int)
                df['minute'] = time_strs.str.split(":").str[1].astype(int)
                df['second'] = time_strs.str.split(":").str[2].astype(float).astype(int)
                dt = pd.to_datetime(df[['year', 'month', 'day', 'hour', 'minute', 'second']])
            elif time_strs.str.contains(":").all() and time_strs.str.count(":").eq(1).all():
                df['hour'] = time_strs.str.split(":").str[0].astype(int)
                df['minute'] = time_strs.str.split(":").str[1].astype(int)
                df['second'] = 0
                dt = pd.to_datetime(df[['year', 'month', 'day', 'hour', 'minute', 'second']])
        else:
            dt = pd.NaT
        df.insert(0, 'datetime', dt) #insert datetime column into df
        columns_to_keep = [col for col in df.columns if col in selected_columns or col == 'datetime'] #only keep whatever columns are in selected_columns
        df_filtered = df[columns_to_keep]
        header = pd.DataFrame.from_dict(data1.headers, orient='index').T #create metedata header from dictionary and repeat it to match lenth of dataset
        header_repeated = pd.concat([header] * len(df_filtered), ignore_index=True)
        # Combine data and metadata
        combined = pd.concat([df_filtered.reset_index(drop=True), header_repeated], axis=1)
        combined = combined.loc[:, ~combined.columns.duplicated()]
        dfs.append(combined)
    #final concatenation
    globals()[str(all_folders[folders])] = pd.concat(dfs, ignore_index=True)
BIGELOW
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[4], line 8
      6 dfs = []  # list to collect all processed DataFrames
      7 for file in f_list:
----> 8     data1 = sb.readSB(filename=file, no_warn=True)
      9     data2 = data1.data
     10     df = pd.DataFrame.from_dict(data2, orient='index').T

NameError: name 'sb' is not defined
cdom_columns =['ag', 'abs_ag', 'cdomf', 'cdmf', 'cdmf_rfu','pim', 'pic', 'spm', 'pom', 'turbidity', 'cdom',] #for reducing rows without these in it 
for names in all_folders: #for each affilitation dataframe
    cdom_cols=[col for col in (globals()[names]).columns if col in cdom_columns] #detect the chlorophyll columns present 
    (globals()[names]).dropna(subset=cdom_cols, how='all', inplace=True) #if rows in the chl columns empty, remove 

#ensure everyone has datetime
for names in all_folders:
    print(names)
    (globals()[names])['time_flag']='no'
    if 'datetime' not in (globals()[names]).columns:
        (globals()[names])['datetime']=pd.NaT
    for idx in (globals()[names]).index:
        try:
            if pd.isna((globals()[names]).at[idx, 'datetime']) and (globals()[names]).at[idx, 'start_date'] == (globals()[names]).at[idx, 'end_date']:
                # Extract strings
                date_str = str((globals()[names]).at[idx, 'end_date'])  # e.g. "20240520"
                time_str = str((globals()[names]).at[idx, 'end_time'])  # e.g. "14:30:00.000"
                # Parse date and time parts
                year = int(date_str[:4])
                month = int(date_str[4:6])
                day = int(date_str[6:8])
                hour = int(time_str[:-11])
                minute = int(time_str[-10:-8])
                second = int(float(time_str[-7:-5]))  # Handles "00.000"

                dt = datetime(year, month, day, hour, minute, second)
                (globals()[names]).at[idx, 'datetime'] = dt
                (globals()[names]).at[idx, 'time_flag'] = 'yes'
        except Exception as e:
            # If there's a parsing problem, skip gracefully
            print(f"Row {idx} failed — start_date: {date_str}, end_time: {time_str}")
            (globals()[names]).at[idx, 'time_flag'] = f'error: {e}'

#populate empty lat lon with lat lon from datafram          
for names in all_folders:
    (globals()[names])['coord_flag'] = 'no'    #print(str(a))
    if 'lat' not in (globals()[names]).columns:
        (globals()[names])['lat']=pd.NA
        (globals()[names])['lon']=pd.NA
    for idx in (globals()[names]).index:
        if pd.isna((globals()[names]).at[idx, 'lat']) and (globals()[names]).at[idx, 'north_latitude'] ==(globals()[names]).at[idx, 'south_latitude']:
            (globals()[names]).at[idx, 'lat'] = float((globals()[names]).north_latitude[idx][:-5])
            (globals()[names]).at[idx, 'coord_flag'] = 'yes'
        if pd.isna((globals()[names]).at[idx, 'lon']) and (globals()[names]).at[idx, 'west_longitude'] ==(globals()[names]).at[idx, 'east_longitude']:
            (globals()[names]).at[idx, 'lon'] = float((globals()[names]).west_longitude[idx][:-5])
            (globals()[names]).at[idx, 'coord_flag'] = 'yes'   

               

For each requested_file, run dfs_to_concata and save it to a cdom before repeating for all the requested files you have. Then concatinate them all together.

dfs_to_concat = [(globals()[names]) for names in all_folders] #concatinate the dataframes in all_folders

cdom1 = pd.concat(dfs_to_concat, ignore_index=True) #save dfs_to_concat to single dataframe. STOP HERE and repeat if you have multiple requested files
cdom2 = pd.concat(dfs_to_concat, ignore_index=True)
cdom3 = pd.concat(dfs_to_concat, ignore_index=True)
cdom4 = pd.concat(dfs_to_concat, ignore_index=True)
cdom5 = pd.concat(dfs_to_concat, ignore_index=True)
cdom_all= pd.concat([cdom1,cdom2,cdom3,cdom4,cdom5])
cdom_all['lat'] = pd.to_numeric(cdom_all['lat'], errors='coerce')
cdom_all['lon'] = pd.to_numeric(cdom_all['lon'], errors='coerce')

Next, only save the columns we want to keep and ensure all data is in the shapefile

cdom_sb = cdom_all[['datetime', 'lon', 'lat', 'identifier_product_doi', 'affiliations', 'investigators', 'contact', 'experiment', 'cruise',
                     'data_type',  'station','depth', 'time_flag', 'coord_flag','pim', 'pic', 'spm', 'pom', 'cdom']]    
cdom_sb = cdom_sb.dropna(subset=['cdom','spm','pim', 'pom',], how='all')
cdom_sb = cdom_sb.dropna(subset=['lat'], how='all')
cdom_sb = cdom_sb.dropna(subset=['datetime'], how='all').reset_index(drop=True)

shp = gpd.read_file(r'C:\Users\gianna.milton\Documents\Python\Shapefiles\combined_coastline.shp')
gdf = gpd.GeoDataFrame(cdom_sb, geometry=gpd.points_from_xy(cdom_sb.lon, cdom_sb.lat), crs="EPSG:4269")
gdf = gdf.to_crs(shp.crs)
cdom_sb = gpd.sjoin(gdf, shp, how="inner", predicate="within")
columns_to_drop = ['geometry', 'index_right', 'merge_id']
cdom_sb = cdom_sb.drop(columns=columns_to_drop)
cdom_sb= cdom_sb.reset_index(drop=True)
cdom_sb['datetime'] = pd.to_datetime(cdom_sb['datetime']) #ensure datetime is in correct format
cdom_sb = cdom_sb[cdom_sb['datetime'] >= '2000-01-01'] #only want data from 2000 on for this algorithm 

cdom_sb['depth'] = pd.to_numeric(cdom_sb['depth'], errors='coerce')
cdom_sb = cdom_sb[cdom_sb['depth'] <=150].reset_index(drop=True)

cdom_sb = cdom_sb.dropna(axis=1, how='all')

Plots#

seabass = pd.read_excel(r'C:\Users\gianna.milton\Documents\Python\Coastal_chl_final\SB_cdom_na.xlsx')
seabass = seabass.dropna(subset=['cdom'])
year_test=seabass.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/b2425041cf16bba7a8d1e817acf84352df3760b7b8a36aeb1739bf13f4cf2c4b.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=15, 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/7ef8a6705b11e89420bd37c2ddc89bb097f3894ce99d88af516ceab8810e0c88.png