Matchup to OC-CCI#

The completed chlorophyll dataset is next matched to the Ocean Colour Climate Change Initiative (OC-CCI) chlorophyll data, with 4km spatial resolution, and Optical Water Types (OWTs) derived from the OC-CCI Rrs data (wavelengths = 412, 443, 490, 510, 560, 665). Due to the size of the in situ dataframe and the size of the satellite data, many steps had to be taken to easy the data processing. This includes subsetting the satellite data to only North American regions, only pulling relevant dates, and processing the matchup one year at a time.

import numpy as np
import pandas as pd
import xarray as xr
from datetime import datetime,date, timedelta
import time
import warnings
warnings.filterwarnings('ignore', category=pd.errors.SettingWithCopyWarning)
warnings.filterwarnings('ignore', category=FutureWarning)
import os
from scipy import stats
import math
from math import radians, sin, cos, sqrt, atan2
import datetime as dt 
from scipy import stats

First, match insitu chlorophyll to OC-CCI chlorophyll#

The function, “haversine_distance_vectorized” is used to calculate the distance between and in situ coordinate and a satellite coordinate to ensure the matchups are reasonably far apart (6km in this case).

def haversine_distance_vectorized(lat1, lon1, lat2, lon2):
    """
    Calculate the haversine distance between two sets of points in a vectorized way.
    Parameters:
        lat1, lon1: Latitude and longitude of the first set of points
        lat2, lon2: Latitude and longitude of the second set of points 
    Returns:
        Array of distances in kilometers.
    """
    R = 6371  #radius of earth
    #convert latitude and longitude from degrees to radians
    lat1_rad, lon1_rad = np.radians(lat1), np.radians(lon1)
    lat2_rad, lon2_rad = np.radians(lat2), np.radians(lon2)

    #calculate differences in coordinates
    dlon = lon2_rad - lon1_rad
    dlat = lat2_rad - lat1_rad

    #haversine formula
    a = np.sin(dlat / 2)**2 + np.cos(lat1_rad) * np.cos(lat2_rad) * np.sin(dlon / 2)**2
    c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a))
    distance = R * c
    return distance

The “slice_before_concat” function is used to subset the satellite xarrays from global to the North American region.

def slice_before_concat(ds):
        """
        Slices the xarray dataset to keep only a specific lat/lon range instead of loading in global data
        NOTE: 'lat' and 'lon' here need to have same naming convention as the satellite nc file
        """
        ds = ds.sel(lat=slice(80,10),lon=slice(-179,-40)) #slice xarray within these lat and lon bounds 
        return ds

The “insitu_year” function is used to reduce the in situ dataframe to a given year, and to pull the specific dates to only gather relevant satellite files. We subset the in situ dataframe to a year to aid in processing power as matching the whole dataframe at once was too memory intensive.

def insitu_year(df,year):
    ''' 
    Takes a dataframe and given year, returns dataframe with only data from that year and list of YYYmmdd times to pull xarray file names 
    df = in situ dataframe, with datetime column
    year = desired year
    returns:
    filtered_df= in situ dataframe subset to the given year
    dates = list of unique dates in format YYYYmmdd
    '''
    year = str(year) #ensure year is string
    start_date = pd.Timestamp(year+'-01-01 00:00:00') #jan 1st of year
    end_date = pd.Timestamp(year+'-12-31 23:59:59') #dec 31st of year
    filtered_df = df[df['datetime'].between(start_date,end_date)].reset_index(drop=True) #only pull between these dates from df
    unique_dates=filtered_df.copy()
    unique_dates['date'] = unique_dates.datetime.dt.date #create column of just dates without times
    unique_dates = unique_dates.drop_duplicates(subset=['date'], keep='first').astype(str) #find all unique dates and turn to str
    unique_dates['date'] = unique_dates['date'].str.replace('-', '') #take out all - 
    dates = unique_dates.date #return just the dates as an array 
    return filtered_df, dates

The function “sat_insit_match3” is used to match the satellite xarray to the in situ dataframe. The output of this function is a dataframe with the same in situ columns, but with additional satellite columns such as chl_occci (the OC-CCI chlorophyll value), matched_sat_time (the time of the satellite matchup), and matched_spatial_dist_km (the distance between the satellite point and the in situ point).

def sat_insit_match3(sat_xr, insit_df):
    ''' takes a satellite xarray and a in situ dataframe and matches datapoinst with same lat/lon/datetime 
        sat_xr: satellite xarray subset to only unique dates from in situ dataframe
        insit_df: in situ dataframe subset to specific year 
        return: insitu dataframe with additional colum for satellite matches 
    '''
    time_indexer = xr.DataArray(insit_df['datetime'], dims='point') #pull all the dates and turn into array
    lat_indexer = xr.DataArray(insit_df['lat'], dims='point') #pull all lats and turn into an array
    lon_indexer = xr.DataArray(insit_df['lon'], dims='point') #pull all lons and turn into an array 

    print("Finding nearest satellite points for all in-situ data at once...")
    #use xarray.sel to gather the same lat, lon, and time points as the in situ dataset from the satellite data
    matched_sat_points = sat_xr.sel(time=time_indexer, lat=lat_indexer,lon=lon_indexer,method='nearest')
    
    print("Filtering matches based on spatial and temporal criteria...")
    #calculate the distance in the matched sat point is from the in situ point.
    distances_km = haversine_distance_vectorized( insit_df['lat'].values,  insit_df['lon'].values, matched_sat_points['lat'].values,
                                                 matched_sat_points['lon'].values)

    #check if the match is on the exact same day.
    time_match_mask = insit_df['datetime'].dt.date.values == pd.to_datetime(matched_sat_points['time'].values).date

    #create mask that only retains data where the dates match. 
    valid_indices = np.where(time_match_mask)[0]
    print(f"Found {len(valid_indices)} valid matches out of {len(insit_df)} points. Now extracting 3x3 grids...")
    
    #initiate columns to put satellite information 
    insit_df['chl_occci'] = np.nan
    insit_df['matched_sat_time'] = pd.NaT
    insit_df['matched_spatial_dist_km'] = np.nan

    #Gather surrounding 3x3 pixels and find the average 
    if len(valid_indices) > 0: #as long as there are points that pass the date mask
        sat_lat_coords = sat_xr['lat'].values ##grab the lat and lon points from WHOLE xarray
        sat_lon_coords = sat_xr['lon'].values

        #loop ONLY over the valid matches to extract the 3x3 grid.
        for idx in valid_indices: 
            #get the coordinates of the valid matched satellite point.
            matched_lat = matched_sat_points['lat'].values[idx]
            matched_lon = matched_sat_points['lon'].values[idx]
            matched_time = matched_sat_points['time'].values[idx]

            #using the coordinates from the valid matches, gather the index value that matches to the whole xarray
            lat_idx = sat_xr.indexes['lat'].get_loc(matched_lat)
            lon_idx = sat_xr.indexes['lon'].get_loc(matched_lon)
            time_idx = sat_xr.indexes['time'].get_loc(matched_time)

            #gather slice information the surrounding lat and lon pixels 
            lat_slice = slice(max(0, lat_idx - 1), min(len(sat_lat_coords), lat_idx + 2))
            lon_slice = slice(max(0, lon_idx - 1), min(len(sat_lon_coords), lon_idx + 2))
            
            #extract the 3x3 grid using isel.
            grid_data = sat_xr['chlor_a'].isel(time=time_idx, lat=lat_slice, lon=lon_slice) #gather the chlorophyll data from surrounding pixels
            grid_values = grid_data.values.flatten() #flatten array
            
            #calculate the mean of the grid
            valid_grid_values = grid_values[~np.isnan(grid_values)] #remove any nans from the array
            grid_mean = np.mean(valid_grid_values) if valid_grid_values.size > 0 else np.nan #as long as the array has a value, find the mean, else set as nan

            #store the results in the DataFrame.
            insit_df.loc[idx, 'chl_occci'] = grid_mean
            insit_df.loc[idx, 'matched_sat_time'] = matched_time
            insit_df.loc[idx, 'matched_spatial_dist_km'] = distances_km[idx]

    print('Finished matching process!')
    return insit_df

The “insit_match” function takes the chlorophyll dataframe and a given year and subsets the in situ dataframe and satellite xarray to given dates in the year, creates a single satllite xarray, and runs all the above functions to return the final matched dataframe for a given year.

def insit_match(insitu_df,year):
    '''
    code to gather and concat all xarray files with matching dates to the in situ dataset and subset to north america
    input: 
        insitu_df: dataframe with datetime, lat, lon, and variable
        year: the specific year to subset the dataframe to 
    returns:
        matched_chl:dataframe subset to that specific year with matched OC-CCI chlorophyll
    '''
    #first, use the function insitu_year to subset the in situ dataframe to specific year. this helps with processing
    insit_year, dates_year= insitu_year(insitu_df,year)
    
    existing_files= []
    for date in sorted(dates_year): #for each date that recorded data 
        file_date = r'CHL\ESACCI-OC-L3S-CHLOR_A-MERGED-1D_DAILY_4km_GEO_PML_OCx-'+str(date)+'-fv6.0.nc' #load in the specific nc file name
        if os.path.exists(file_date): #if the file exists, then add to a list of files
            existing_files.append(file_date)
        else:
            print(f"Warning: File not found {date}: {file_date}") #else, skip 
            
#If there are some issues with corrupted files, comment out the above block from "existing_files" to "print", and uncomment the below block
    #existing_files= []
    #for date in sorted(dates_year):
    #    file_date = r'CHL\ESACCI-OC-L3S-CHLOR_A-MERGED-1D_DAILY_4km_GEO_PML_OCx-'+str(date)+'-fv6.0.nc'
    #    if os.path.exists(file_date): #if the file exists, try to open and load it in. 
    #        try:
    #            temp_ds = xr.open_dataset(file_date, chunks={}) #open the file
     #           _ = temp_ds['chlor_a'].isel(time=0, latitude=0, longitude=0).compute() #load in the data instead of lazy load to make sure it exists.
     #           temp_ds.close() #close the file
     #           existing_files.append(file_date) #if it oppened, append to the list of files 
     #       except Exception as e: #if it didn't open, then skip. 
     #           print(f"Skipping corrupted or unreadable file: {file_date}") #. Error: {e}
                #temp_ds.close()
     #   else:
     #        print(f"Warning: File not found for date {date}: {file_date}") #if the file name doesn't exist, skip 
    
    #concat all satellite .nc files into single file       
    coastal_us_combo= xr.open_mfdataset(existing_files, preprocess=slice_before_concat,combine='by_coords') #open all files, concat into one subset to north america
    print('satellite refiguring complete') 
    test_insitu = insit_year.copy().reset_index(drop=True) #subset dataframe to specific year, reset the index
    matched_chl = sat_insit_match3(coastal_us_combo, test_insitu) #run the satellite matching function
    coastal_us_combo.close() #close the file
    return matched_chl  #matched_chl = dataframe of in situ chlorophyll to OC-CCI chlorophyll 

Run the Chlorophyll matchups#

chl_all = pd.read_excel(r"Coastal_chl_final\all_chl.xlsx") #load in concatinated chlorophyll dataset.
chl_all['datetime'] = chl_all['datetime'].apply(pd.to_datetime) #ensure datetime is in pandas format 
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[7], line 1
----> 1 chl_all = pd.read_excel(r"Coastal_chl_final\all_chl.xlsx") #load in concatinated chlorophyll dataset.
      2 chl_all['datetime'] = chl_all['datetime'].apply(pd.to_datetime)

File ~\AppData\Local\anaconda3\Lib\site-packages\pandas\io\excel\_base.py:495, in read_excel(io, sheet_name, header, names, index_col, usecols, dtype, engine, converters, true_values, false_values, skiprows, nrows, na_values, keep_default_na, na_filter, verbose, parse_dates, date_parser, date_format, thousands, decimal, comment, skipfooter, storage_options, dtype_backend, engine_kwargs)
    493 if not isinstance(io, ExcelFile):
    494     should_close = True
--> 495     io = ExcelFile(
    496         io,
    497         storage_options=storage_options,
    498         engine=engine,
    499         engine_kwargs=engine_kwargs,
    500     )
    501 elif engine and engine != io.engine:
    502     raise ValueError(
    503         "Engine should not be specified when passing "
    504         "an ExcelFile - ExcelFile already has the engine set"
    505     )

File ~\AppData\Local\anaconda3\Lib\site-packages\pandas\io\excel\_base.py:1550, in ExcelFile.__init__(self, path_or_buffer, engine, storage_options, engine_kwargs)
   1548     ext = "xls"
   1549 else:
-> 1550     ext = inspect_excel_format(
   1551         content_or_path=path_or_buffer, storage_options=storage_options
   1552     )
   1553     if ext is None:
   1554         raise ValueError(
   1555             "Excel file format cannot be determined, you must specify "
   1556             "an engine manually."
   1557         )

File ~\AppData\Local\anaconda3\Lib\site-packages\pandas\io\excel\_base.py:1402, in inspect_excel_format(content_or_path, storage_options)
   1399 if isinstance(content_or_path, bytes):
   1400     content_or_path = BytesIO(content_or_path)
-> 1402 with get_handle(
   1403     content_or_path, "rb", storage_options=storage_options, is_text=False
   1404 ) as handle:
   1405     stream = handle.handle
   1406     stream.seek(0)

File ~\AppData\Local\anaconda3\Lib\site-packages\pandas\io\common.py:882, in get_handle(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)
    873         handle = open(
    874             handle,
    875             ioargs.mode,
   (...)
    878             newline="",
    879         )
    880     else:
    881         # Binary mode
--> 882         handle = open(handle, ioargs.mode)
    883     handles.append(handle)
    885 # Convert BytesIO or file objects passed with an encoding

FileNotFoundError: [Errno 2] No such file or directory: 'Coastal_chl_final\\all_chl.xlsx'
chl_test=chl_all.loc[chl_all['depth']<=10]  #since OC-CCI is sattellite data and only records 1 depth, reduce to just surface waters 
#next, since this dataset has flourecence along with extracted, only keep 1 datapoint per datetime, lat, and lon. 
#That way (as shown in next cell with variable 'a'), large sample resolutions don't skew the dataset
chl_test=chl_test.drop_duplicates(subset=['datetime', 'lat', 'lon'], keep='first').reset_index(drop=True) 
#keep the shallowest sample within the top 10 meters 

#run below code to see what years need to be run. 
unique_dates=chl_test.copy() #copy dataframe to ensure no accidental changes happen to the original dataframe
unique_dates['date'] = unique_dates.datetime.dt.date #create column of just date
unique_dates = unique_dates.drop_duplicates(subset=['date'], keep='first').astype(str).reset_index(drop=True) #find all unique dates and turn to str
unique_dates['date'] = unique_dates['date'].str.replace('-', '') #take out all - 
dates_year = unique_dates.date
a=chl_all.groupby(['datetime','lat','lon']).size().reset_index(name='counts') #how many samples are recorded in each datetime, lat, and lon. 
a.nlargest(4, 'counts')
datetime lat lon counts
8721 2001-04-10 12:34:00 35.20177 -76.24203 315
26707 2003-04-30 00:51:58 34.15600 -119.94900 300
32321 2003-09-11 03:07:41 34.29800 -119.88500 300
34475 2003-11-05 05:08:44 34.34300 -119.86500 300

As mentioned above, the matchup process is done in parts (years) to aid in processing

chl_2000 =insit_match(chl_test,2000) #run insit_match to the chlorophyll data, subset to 2000. repeat for all years
chl_2000 = chl_2000.dropna(subset=['chl_occci']) #remove any empty OC-CCI matches, since we only want datapoints that correspond.
chl_2001 =insit_match(chl_test,2001)
chl_2001 = chl_2001.dropna(subset=['chl_occci']) 
chl_2002 =insit_match(chl_test,2002)
chl_2002 = chl_2002.dropna(subset=['chl_occci'])
chl_2003 =insit_match(chl_test,2003)
chl_2003 = chl_2003.dropna(subset=['chl_occci'])
chl_2004 =insit_match(chl_test,2004)
chl_2004 = chl_2004.dropna(subset=['chl_occci'])
chl_2005 =insit_match(chl_test,2005)
chl_2005 = chl_2005.dropna(subset=['chl_occci'])
chl_2006 =insit_match(chl_test,2006)
chl_2006 = chl_2006.dropna(subset=['chl_occci'])
chl_2007 =insit_match(chl_test,2007)
chl_2007 = chl_2007.dropna(subset=['chl_occci'])
chl_2008 =insit_match(chl_test,2008)
chl_2008 = chl_2008.dropna(subset=['chl_occci'])
chl_2009 =insit_match(chl_test,2009)
chl_2009 = chl_2009.dropna(subset=['chl_occci'])
chl_2010 =insit_match(chl_test,2010)
chl_2010 = chl_2010.dropna(subset=['chl_occci'])
chl_2011 =insit_match(chl_test,2011)
chl_2011 = chl_2011.dropna(subset=['chl_occci'])
chl_2012 =insit_match(chl_test,2012)
chl_2012 = chl_2012.dropna(subset=['chl_occci'])
chl_2013 =insit_match(chl_test,2013)
chl_20103 = chl_2013.dropna(subset=['chl_occci'])
chl_2014 =insit_match(chl_test,2014)
chl_2014 = chl_2014.dropna(subset=['chl_occci'])
chl_2015 =insit_match(chl_test,2015)
chl_2015 = chl_2015.dropna(subset=['chl_occci'])
chl_2016 =insit_match(chl_test,2016)
chl_2016 = chl_2016.dropna(subset=['chl_occci'])
chl_2017 =insit_match(chl_test,2017)
chl_2017 = chl_2017.dropna(subset=['chl_occci'])
chl_2018 =insit_match(chl_test,2018)
chl_2018 = chl_2018.dropna(subset=['chl_occci'])
chl_2019 =insit_match(chl_test,2019)
chl_2019 = chl_2019.dropna(subset=['chl_occci'])
chl_2020 =insit_match(chl_test,2020)
chl_2020 = chl_2020.dropna(subset=['chl_occci'])
chl_2021 =insit_match(chl_test,2021)
chl_2021 = chl_2021.dropna(subset=['chl_occci'])
chl_2022 =insit_match(chl_test,2022)
chl_2022 = chl_2022.dropna(subset=['chl_occci'])
chl_2023 =insit_match(chl_test,2023)
chl_2023 = chl_2023.dropna(subset=['chl_occci'])
chl_2024 =insit_match(chl_test,2024)
chl_2024 = chl_2024.dropna(subset=['chl_occci'])
chl_2025 =insit_match(chl_test,2025)
chl_2025 = chl_2025.dropna(subset=['chl_occci'])
#since this dataset takes so long to run, everytime a year is matched added it to the existing matchups and save 
current_matchup = pd.read_excel('sat_matchupCHL2.xlsx')  #load in current matchup
dfs=[current_matchup,chl_2025] #append the dataframe to the existing matched dataframe 
matched_all = pd.concat(dfs).reset_index(drop=True)
matched_all.to_excel("sat_matchupCHL2.xlsx", index = False) #export dataframe 

Next, matchup the matched dataset to OWT dataset#

OWT .nc files were already created from the OC-CCI Rrs data. Some matchup functions can be reused from the chlorophyll matchup, but some have to be slightly tweaked to accuratly matchup the dataset. First, “slice_before_concat” needs to switch from lat and lon to latitude and longitude to match the xarray variable names.

def slice_before_concat(ds):
        """
        Slices the xarray dataset to keep only a specific lat/lon range instead of loading in global data
        NOTE: 'lat' and 'lon' here need to have same naming convention as the satellite nc file
        """
        ds = ds.sel(latitude=slice(80,10),longitude=slice(-179,-40)) #slice xarray within these lat and lon bounds 
        return ds

Additionally, “sat_insit_match3” needs to be tweaked to retain OWT matchups, not chlorophyll matchups. It returns the matched dataframe along with columns ‘owt’ (mode of the surrounding 3x3 pixels) and ‘all_classes’ (array of all OWTs in grid). This functions finds the mode of the surrounding 3x3 pixels rather than the mean. So for example, if the 3x3 grid looks like:

owt_grid

then the owt = 12 and the all_classes = [10.0, 12.0, 12.0, 10.0, 12.0, 12.0, 9.0, 12.0, 12.0]

def sat_insit_match3(sat_xr, insit_df, spatial_diff_km,variable):
    ''' takes a satellite xarray and a in situ dataframe and finds datapoints with same lat/lon/datetime withing bounds
        sat_xr: satellite satellite xarray subset to only unique dates from in situ dataframe
        insit_df: in situ dataframe subset to specific year 
        spatial_diff_km: spatial boundary for matching in km
        var: satellite variable name to match
        return: insitu dataframe with additional colum for satellite matches 
    '''
    #First make sure the in situ dataset has the right datetime format 
    insit_df['datetime'] = pd.to_datetime(insit_df['datetime'], errors='coerce')
    insit_df.dropna(subset=['datetime'], inplace=True) #remove any empty datetime rows
    insit_df.dropna(subset=['lat'], inplace=True) #remove rows without complete coordinates
    insit_df.reset_index(inplace=True, drop=True)
    
    time_indexer = xr.DataArray(insit_df['datetime'], dims='point') #pull all the dates and turn into array
    lat_indexer = xr.DataArray(insit_df['lat'], dims='point') #pull all lats and turn into an array
    lon_indexer = xr.DataArray(insit_df['lon'], dims='point') #pull all lons and turn into an array 

    print("Finding nearest satellite points for all in-situ data at once...")
    #use xarray.sel to gather the same lat, lon, and time points as the in situ dataset from the satellite data
    matched_sat_points = sat_xr.sel(time=time_indexer, latitude=lat_indexer,longitude=lon_indexer, method='nearest')

    print("Filtering matches based on spatial and temporal criteria...")
    #calculate the distance in the matched sat point is from the in situ point.
    distances_km = haversine_distance_vectorized(insit_df['lat'].values, insit_df['lon'].values, matched_sat_points['latitude'].values,
                                                matched_sat_points['longitude'].values)

    #check if the match is on the exact same day.
    time_match_mask = insit_df['datetime'].dt.date.values == pd.to_datetime(matched_sat_points['time'].values).date
    
    #check if the match is within 6km of the in situ coordinates 
    spatial_match_mask = distances_km <= spatial_diff_km

    #combine the two checks to create a mask where the dates match and within 6kms
    valid_match_mask = time_match_mask & spatial_match_mask

    #initiate columns to put satellite information 
    insit_df['owt'] = np.nan
    insit_df['all_classes'] = np.nan
    insit_df['all_classes'] = insit_df['all_classes'].astype(object)

    #create mask that only retains data where the dates match. 
    valid_indices = np.where(valid_match_mask)[0]
    print(f"Found {len(valid_indices)} valid matches out of {len(insit_df)} points. Now extracting 3x3 grids...")
    
    #Gather surrounding 3x3 pixels and find the mode 
    if len(valid_indices) > 0: #as long as there are points that pass the date mask
        sat_lat_coords = sat_xr['latitude'].values #grab the lat and lon points from WHOLE xarray
        sat_lon_coords = sat_xr['longitude'].values

        #loop ONLY over the valid matches to extract the 3x3 grid.
        for idx in valid_indices:
            #get the coordinates of the valid matched satellite point.
            matched_lat = matched_sat_points['latitude'].values[idx]
            matched_lon = matched_sat_points['longitude'].values[idx]
            matched_time = matched_sat_points['time'].values[idx]

            #using the coordinates from the valid matches, gather the index value that matches to the whole xarray
            lat_idx = sat_xr.indexes['latitude'].get_loc(matched_lat)
            lon_idx = sat_xr.indexes['longitude'].get_loc(matched_lon)
            time_idx = sat_xr.indexes['time'].get_loc(matched_time)

            #gather slice information the surrounding lat and lon pixels 
            lat_slice = slice(max(0, lat_idx - 1), min(len(sat_lat_coords), lat_idx + 2))
            lon_slice = slice(max(0, lon_idx - 1), min(len(sat_lon_coords), lon_idx + 2))
            
            #extract the 3x3 grid using isel.
            grid_data = sat_xr[variable].isel(time=time_idx, latitude=lat_slice, longitude=lon_slice) #gather the OWT data from surrounding pixels
            grid_values = grid_data.values.flatten() #flatten array
            
            #calculate the mode, ignoring NaN values.
            valid_grid_values = grid_values[~np.isnan(grid_values)] #remove any nans from the array
            grid_mode = stats.mode(valid_grid_values).mode if valid_grid_values.size > 0 else np.nan #as long as the array has a value, find the mode, else set as nan

            #store the results in the DataFrame.
            insit_df.loc[idx, 'owt'] = grid_mode
            insit_df.at[idx, 'all_classes'] = grid_values.tolist()

    print('Finished matching process!')
    return insit_df

We’ll also slightly alter the final matchup function to grab OWT file names.

def owt_insit_match(insitu_df, sat_xr, spatial_res, variable, year):
    '''
    code that produces dataframe of matching satellite points and insitu points.
    input: insitu_df: dataframe with datetime, lat, lon, and variables
        sat_xr: folder string to satellite data files
        spatial_res: resolution boundry for matching (km)
        variable: the variable being matched (OWT)
        year: the year in situ and sat data are being compared (int)
    returns:
        in situ dataframe with matched datasets subset to the given year 
    '''
    #first, reduce insitu_df to only stated year 
    insit_year, dates_year= insitu_year(insitu_df,year)

    #next, pull xarray files that match to the dates of the insit_year
    existing_files= []
    for date in sorted(dates_year):
        file_date = str(sat_xr)+str(date)+'-OCCCI-V6.0-GLOBAL_MAPPED-OWC_WEI.nc'
        if os.path.exists(file_date):
            existing_files.append(file_date)
        else:
            print(f"Warning: File not found for date {date}: {file_date}")
            
    ## NOTE: uncomment below if you're getting errors about corrupted files. THis code will open each file to ensure there's actual data inside
    # existing_files= []
    # var = 'water_class'
    # for date in sorted(dates_year):
    #    file_date = str(sat_xr)+str(date)+'-OCCCI-V6.0-GLOBAL_MAPPED-OWC_WEI.nc'
    #    if os.path.exists(file_date): #if path exists, try to open it
    #        try:
    #            temp_ds = xr.open_dataset(file_date, chunks={}) # Use chunks={} for Dask if needed
    #            _ = temp_ds[var].isel(time=0, latitude=0, longitude=0).compute()
    #            temp_ds.close()
    #           existing_files.append(file_date)
    #        except Exception as e:
    #            print(f"Skipping corrupted or unreadable file: {file_date}") #. Error: {e}
                #temp_ds.close()
    #    else:
    #         print(f"Warning: File not found for date {date}: {file_date}")

    
    #concat all satellite .nc files into 1 single xarray file       
    coastal_us_combo= xr.open_mfdataset(existing_files, preprocess=slice_before_concat,combine='by_coords') #open all files, concat into one subset to north america
    print('satellite refiguring complete')
    test_insitu = insit_year.copy()
    matched_chl = sat_insit_match3(coastal_us_combo, test_insitu, spatial_res,variable) #run the satellite matching function
    return matched_chl  #matched_chl = dataframe of in situ chlorophyll to OC-CCI OWT 
#load in all the data with the sattellite matchups 
chl_all = pd.read_excel(r"Coastal_chl_final\sat_matchupCHL2.xlsx")
chl_all['datetime'] = pd.to_datetime(chl_all['datetime'],format='mixed') 
#all reducing already done in first round of matchups 
insit_chl_coast2 = chl_all.copy()
test= insit_chl_coast2['datetime'].dt.year
test.value_counts()
#to match, run matching code for each year
df_matched2000 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2000)
df_matched2001 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2001)
df_matched2002 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2002)
df_matched2003 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2003)
df_matched2004 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2004)
df_matched2005 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2005)
df_matched2006 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2006)
df_matched2007 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2007)
df_matched2008 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2008)
df_matched2009 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2009)
df_matched2010 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2010)
df_matched2011 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2011)
df_matched2012 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2012)
df_matched2013 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2013)
df_matched2014 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2014)
df_matched2015 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2015)
df_matched2016 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2016)
df_matched2017 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2017)
df_matched2018 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2018)
df_matched2019 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2019)
df_matched2020 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2020)
df_matched2021 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2021)
df_matched2022 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2022)
df_matched2023 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2023)
df_matched2024 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2024)
df_matched2025 = owt_insit_match(insit_chl_coast2, r'OWC_WEI\D_',6, 'water_class', 2025)
current_matchup = pd.read_excel(r'Coastal_chl_final\owt_allchlV3.xlsx')
dfs=[current_matchup,df_matched2023] 
matched_all = pd.concat(dfs).reset_index(drop=True)
matched_all.to_excel("owt_allchlV3.xlsx", index = False)