Appendix D — Python Notebook Part 1

Part 1: Fishing and Climate Data Analysis & Construction of Fishable Days Index (FDI)

D.1 Import Packages & Set Constants

# Import packages
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import seaborn as sns # statistical visualization library

# Sparse daily fishing reports file path
FISHING_PATH = "../data/stx_com_csl_daily_8623.csv"

# Hourly ERA5 wind components
WIND_PATH = "../data/st_croix_wind_data.csv"

# Hourly ERA5 significant wave height
WAVE_PATH = "../data/st_croix_wave_data.csv"

# Annual VI population
POPULATION_PATH = "../data/usvi_pop.csv"

# Date range
START_DATE = "1985-01-01" # first day of study period
END_DATE   = "2024-12-28" # last day of study period

# Unit conversion
MS_TO_KNOTS = 1.94384449 # m/s to knots conversion factor

# Fishable day thresholds (initial values, tunable in Section 9)
WIND_THRESHOLD_MS = 8.5 #10.0 # max wind speed in m/s (~19.44 knots)
WAVE_THRESHOLD_M  = 1.75 #2.0 # max significant wave height in meters

# Population scaling denominator
POP_SCALE = 1000 # rates per 1k people

D.2 Load Raw Climate and Fishing Data

# Fishing data: read daily fishing reports, parse date column to datetime
df_fishing = pd.read_csv(
  FISHING_PATH, 
  parse_dates=["date"],
  date_format="%m/%d/%Y"
  )

# Keep only relevant columns
df_fishing = df_fishing[["date", "weekday", "n_trips", "n_csl_trips",
                          "all_all", "csl_all"]]
                          
# Wind data read hourly wind, parses timestamp column
df_wind = pd.read_csv(WIND_PATH, parse_dates=["valid_time"])

# Keep only relevant columns
df_wind = df_wind[["valid_time", "u10", "v10", "fg10"]]

# Wave data: read hourly wave heights, parses timestamp column
df_wave = pd.read_csv(WAVE_PATH, parse_dates=["valid_time"])

# Keep only relevant columns
df_wave = df_wave[["valid_time", "swh"]]

# Population data: read annual population, parse date column
df_pop = pd.read_csv(POPULATION_PATH, parse_dates=["observation_date"])

# Rename columns
df_pop = df_pop.rename(columns={"observation_date": "year",
                                 "POPTOTVIA647NWDB": "population"})

# Extract integer year from date for merging later
df_pop["year"] = df_pop["year"].dt.year

# Check
print("Fishing rows:", len(df_fishing)) # should be less than 14,608
print("Wind rows:",    len(df_wind))    # should be ~350,000+ hourly rows
print("Wave rows:",    len(df_wave))    # should be ~350,000+ hourly rows
print("Population rows:", len(df_pop)) # should be 40 rows, one per year
Fishing rows: 12834
Wind rows: 350568
Wave rows: 350568
Population rows: 40

D.3 Construct Completed Time-Series to Fill Non-Reported Dates From Fishing Dataset

# Build full dataset structure filling fishing days not reported: 1 row per day
all_dates = pd.DataFrame(
    {"date": pd.date_range(start=START_DATE, end=END_DATE, freq="D")}
)

all_dates["date"] = pd.to_datetime(all_dates["date"], format="%m/%d/%Y")
df_fishing["date"] = pd.to_datetime(df_fishing["date"], format="%m/%d/%y")

# Left join fishing data to full dataset structure: unreported  days become NaN
df_calendar = all_dates.merge(df_fishing, on="date", how="left")

# Fill silent day numeric columns with 0
numeric_cols = ["n_trips", "n_csl_trips", "all_all", "csl_all"]
df_calendar[numeric_cols] = df_calendar[numeric_cols].fillna(0)

# Get weekday from date using pandas dt accessor; overrides sparse file value
df_calendar["weekday"] = df_calendar["date"].dt.day_name()

# Add helper integer year column for population join
df_calendar["year"]  = df_calendar["date"].dt.year

# Add helper integer month column for wind & wave join
df_calendar["month"] = df_calendar["date"].dt.month

# Check
print("Calendar rows:", len(df_calendar))  # should be exactly 14,608
print("Silent days (no trips reported):",
      (df_calendar["n_trips"] == 0).sum()) # days with zero reports
      
# Preview first 5 rows
print(df_calendar.head(5))
Calendar rows: 14607
Silent days (no trips reported): 1773
        date    weekday  n_trips  n_csl_trips  all_all  csl_all  year  month
0 1985-01-01    Tuesday     13.0          0.0    438.0      0.0  1985      1
1 1985-01-02  Wednesday     16.0          0.0    625.0      0.0  1985      1
2 1985-01-03   Thursday     16.0          0.0    565.0      0.0  1985      1
3 1985-01-04     Friday     19.0          0.0    894.0      0.0  1985      1
4 1985-01-05   Saturday     27.0          0.0   1014.0      0.0  1985      1

D.4 Process Wind Dataset

# Calculate scalar wind speed from U and V components in m/s; (u^2 + v^2)^(1/2)
df_wind["wind_speed_ms"] = np.sqrt(df_wind["u10"]**2 + df_wind["v10"]**2) 

# Convert scalar wind speed & gust --> knots
df_wind["wind_speed_kt"] = df_wind["wind_speed_ms"] * MS_TO_KNOTS

# Convert scalar wind speed & gust --> knots
df_wind["wind_gust_kt"]  = df_wind["fg10"]  * MS_TO_KNOTS 

# Extract date from hourly timestamp for grouping: strip time component
df_wind["date"] = df_wind["valid_time"].dt.normalize()    

# Aggregate hourly to daily maximum
df_wind_daily = (
    df_wind.groupby("date")
    .agg(
        # daily max scalar wind speed in m/s
        max_wind_ms = ("wind_speed_ms", "max"),   
        # daily max scalar wind speed in knots
        max_wind_kt = ("wind_speed_kt", "max"),   
        # daily max gust in m/s
        max_gust_ms = ("fg10",          "max"),  
        # daily max gust in knots
        max_gust_kt = ("wind_gust_kt",  "max"),  
    )
    # bring date back as a regular column
    .reset_index()                                
)

# Check
print("Daily wind rows:", len(df_wind_daily)) # should be ~14,608
print("Max wind speed recorded (m/s):", df_wind_daily["max_wind_ms"].max(),3)
print("Max wind speed recorded (kt):",  df_wind_daily["max_wind_kt"].max(),3)

# Preview first 5 rows
print(df_wind_daily.head(5)) 
Daily wind rows: 14607
Max wind speed recorded (m/s): 28.639624763784898 3
Max wind speed recorded (kt): 55.67097679275083 3
        date  max_wind_ms  max_wind_kt  max_gust_ms  max_gust_kt
0 1985-01-01    10.975895    21.335433    15.157806    29.464418
1 1985-01-02    10.659820    20.721032    14.236787    27.674100
2 1985-01-03    10.643230    20.688784    14.280316    27.758714
3 1985-01-04     9.447021    18.363539    12.966755    25.205355
4 1985-01-05     6.931456    13.473673     9.752116    18.956597

D.5 Process Wave Dataset

# Extract date from hourly timestamp column for grouping: strip time component
df_wave["date"] = df_wave["valid_time"].dt.normalize()

# Aggregate the hourly to daily maximums
df_wave_daily = (
    df_wave.groupby("date")
    .agg(
        # daily max. swh in metres
        max_swh_m = ("swh", "max"),                 
    )
    # brings date back as regular column
    .reset_index()                                  
)

# Check
print("Daily wave rows:", len(df_wave_daily)) # should be ~14,608
print("Max wave height recorded (m):", 
      round(df_wave_daily["max_swh_m"].max(), 3))
print("Min wave height recorded (m):", 
      round(df_wave_daily["max_swh_m"].min(), 3))

# Preview first 3 rows
print(df_wave_daily.head(3))                                          
Daily wave rows: 14607
Max wave height recorded (m): 9.329
Min wave height recorded (m): 0.539
        date  max_swh_m
0 1985-01-01   2.061214
1 1985-01-02   1.997108
2 1985-01-03   2.103769

D.6 Merge Wind/Wave/Population Datasets into Fishing Dataset

# Join wind into completed dates frame: left join kept all >14k calendar days
df_master = df_calendar.merge(df_wind_daily, on="date", how="left")

# Join wave into completed dates frame: retain all calendar days
df_master = df_master.merge(df_wave_daily, on="date", how="left")

# Join pop. onto completed dates by  integer year; days assigned annual pop.
df_master = df_master.merge(df_pop, on="year", how="left")

# Check for missing climate data after the merges: days with no wind data
wind_missing = df_master["max_wind_ms"].isna().sum()

# Check for missing climate data after the merges: days with no wave data
wave_missing = df_master["max_swh_m"].isna().sum()

# Check for missing climate data after the merges: days with no population data
pop_missing  = df_master["population"].isna().sum()

print("Days missing wind data:", wind_missing)
print("Days missing wave data:", wave_missing)
print("Days missing population data:", pop_missing)

# Fill missing climate data as a conservative gap: fwd last known wind value
df_master["max_wind_ms"] = df_master["max_wind_ms"].ffill()
df_master["max_wind_kt"] = df_master["max_wind_kt"].ffill()
df_master["max_gust_ms"] = df_master["max_gust_ms"].ffill()
df_master["max_gust_kt"] = df_master["max_gust_kt"].ffill()

# Fill missing climate data as a conservative gap: fwd last known wave value
df_master["max_swh_m"]   = df_master["max_swh_m"].ffill()

# Reorder: Final completed column order
df_master = df_master[["date", "year", "month", "weekday",
                       "n_trips", "n_csl_trips", "all_all", "csl_all",
                       "max_wind_ms", "max_wind_kt",
                       "max_gust_ms", "max_gust_kt",
                       "max_swh_m", "population"]]

# Check
print("\nMaster table shape:", df_master.shape) # should be (14608, 14)

# Preview first 3 rows
print(df_master.head(3))
Days missing wind data: 0
Days missing wave data: 0
Days missing population data: 0

Master table shape: (14607, 14)
        date  year  month    weekday  n_trips  n_csl_trips  all_all  csl_all  \
0 1985-01-01  1985      1    Tuesday     13.0          0.0    438.0      0.0   
1 1985-01-02  1985      1  Wednesday     16.0          0.0    625.0      0.0   
2 1985-01-03  1985      1   Thursday     16.0          0.0    565.0      0.0   

   max_wind_ms  max_wind_kt  max_gust_ms  max_gust_kt  max_swh_m  population  
0    10.975895    21.335433    15.157806    29.464418   2.061214      100760  
1    10.659820    20.721032    14.236787    27.674100   1.997108      100760  
2    10.643230    20.688784    14.280316    27.758714   2.103769      100760  

D.7 Population Adjustment (per 1k people)

# Lobster trips per 1k people: normalized lobster trip rate
df_master["csl_trips_per_1k"] = (
  (df_master["n_csl_trips"] / df_master["population"]) * POP_SCALE
  )

# Total trips per 1kpeople: normalised total trip rate
df_master["all_trips_per_1k"] = (
  (df_master["n_trips"] / df_master["population"]) * POP_SCALE
  )

# Lobster lbs caught per 1k people: normalised lobster catch rate
df_master["csl_lbs_per_1k"] = (
  (df_master["csl_all"] / df_master["population"]) * POP_SCALE
  )

# Total lbs caught per 1k people: normalised total catch rate
df_master["all_lbs_per_1k"] = (
  (df_master["all_all"] / df_master["population"]) * POP_SCALE
  )

# Check
print("Max csl trips per 1k:", round(df_master["csl_trips_per_1k"].max(), 4))
print("Mean csl trips per 1k on days with any trips:",
      round(df_master.loc[df_master["n_csl_trips"] > 0, 
      "csl_trips_per_1k"].mean(), 4)) # average only over active fishing days
      
# Include silent days and genuinely zero lobster days
print("Days with zero csl trips:", (df_master["n_csl_trips"] == 0).sum())                         

# Preview 5 rows of most important columns
print(df_master[["date", "n_csl_trips", "csl_trips_per_1k",
                  "csl_all", "csl_lbs_per_1k"]].head(5))                                          
Max csl trips per 1k: 0.2215
Mean csl trips per 1k on days with any trips: 0.0678
Days with zero csl trips: 6423
        date  n_csl_trips  csl_trips_per_1k  csl_all  csl_lbs_per_1k
0 1985-01-01          0.0               0.0      0.0             0.0
1 1985-01-02          0.0               0.0      0.0             0.0
2 1985-01-03          0.0               0.0      0.0             0.0
3 1985-01-04          0.0               0.0      0.0             0.0
4 1985-01-05          0.0               0.0      0.0             0.0

D.8 Check for Weekday Bias in Fishing Trips

# Define an ordered weekday list for consistent plotting
weekday_order = ["Monday", "Tuesday", "Wednesday", "Thursday", 
                  "Friday", "Saturday", "Sunday"]

# Calculats mean population-adjusted lobster metrics by weekday
weekday_bias = (
    df_master.groupby("weekday")
    .agg(
        # avg normalised lobster trips by weekday
        mean_csl_trips_per_1k = ("csl_trips_per_1k", "mean"), 
        # avg normalised lobster lbs by weekday
        mean_csl_lbs_per_1k   = ("csl_lbs_per_1k",   "mean"),     
        # % of days that had any lobster trips
        pct_days_with_trips   = ("n_csl_trips", lambda x: (x > 0).mean() * 100)  
    )
    # enforce Monday-Sunday order
    .reindex(weekday_order)                          
    .reset_index()
)

# print full table without row index
print(weekday_bias.to_string(index=False))                   
  weekday  mean_csl_trips_per_1k  mean_csl_lbs_per_1k  pct_days_with_trips
   Monday               0.033722             1.238984            54.362416
  Tuesday               0.041677             1.493190            58.648778
Wednesday               0.045011             1.642619            62.721610
 Thursday               0.043207             1.572531            59.080019
   Friday               0.046304             1.747825            62.817441
 Saturday               0.039874             1.337318            62.434116
   Sunday               0.016240             0.549767            32.118888
# Subplot 1: mean lobster trips per 1k ppl by weekday
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# bar chart of mean trips
axes[0].bar(weekday_bias["weekday"], weekday_bias["mean_csl_trips_per_1k"],
            color="steelblue", edgecolor="black")                      
axes[0].set_title("Mean Lobster Trips per 1,000 People by Weekday")
axes[0].set_xlabel("Day of Week")
axes[0].set_ylabel("Trips per 1,000 People")
# rotate x labels for readability
axes[0].tick_params(axis="x", rotation=45)                             

# Subplot 2: % of days with any lobster trips: activity freq. by weekday 
axes[1].bar(weekday_bias["weekday"], weekday_bias["pct_days_with_trips"],
            color="coral", edgecolor="black")                          
axes[1].set_title("% of Days With Any Lobster Trips by Weekday")
axes[1].set_xlabel("Day of Week")
axes[1].set_ylabel("% of Days Active")
axes[1].tick_params(axis="x", rotation=45)

plt.tight_layout()
plt.show()

# Flag Sundays for exclusion from climate threshold validation
df_master["is_sunday"] = (df_master["weekday"] == "Sunday").astype(int)  
print("Sunday days flagged:", df_master["is_sunday"].sum()) # should be ~2,087
Sunday days flagged: 2086

D.9 Defining the Fishable Day Index (FDI)

# Apply climate thresholds defining fishable days: 1 if wind below threshold
df_master["wind_ok"] = (
  (df_master["max_wind_ms"] <= WIND_THRESHOLD_MS).astype(int)
)

# Apply thresholds defining fishable days: 1 if wave height below threshold
df_master["wave_ok"] = (
  (df_master["max_swh_m"]   <= WAVE_THRESHOLD_M).astype(int)
  )

# 1 if BOTH conditions met; wind & wave observations define fishablye/unfishable
df_master["fdi"] = (df_master["wind_ok"] & df_master["wave_ok"]).astype(int)

# Printout of summary of FDI classification: counts of FDI = 1 days
print("Total fishable days:",   df_master["fdi"].sum())          

# Printout of summary of FDI classification: counts of FDI = 0 days
print("Total unfishable days:", (df_master["fdi"] == 0).sum())         

# Printout of summary of FDI classification: overall count for the fishable rate
print("% fishable:", round(df_master["fdi"].mean() * 100, 2))                     

# Validation; compare activity fishable vs unfishable days; drop Sundays
df_validation = df_master[df_master["is_sunday"] == 0].copy()                 

validation_summary = (
    df_validation.groupby("fdi")
    .agg(
        # mean norm. lobster trips
        mean_csl_trips_per_1k = ("csl_trips_per_1k", "mean"),   
        # mean norm. lobster lbs
        mean_csl_lbs_per_1k   = ("csl_lbs_per_1k",   "mean"),      
        # % of days with any lobster activity
        pct_days_active       = ("n_csl_trips", lambda x: (x > 0).mean() * 100)       
    )
    .reset_index()
)
# Label for readability
validation_summary["fdi"] = (
  validation_summary["fdi"].map({0: "Unfishable", 1: "Fishable"})  
  )
print("\nValidation Summary (Sundays excluded):")
print(validation_summary.to_string(index=False))

# Analysis for the threshold sensitivity: range of wind thresholds in m/s
wind_thresholds = [7.5, 8.5, 10.0, 11.5, 13.0]

# Analysis for the threshold sensitivity: range of wave thresholds in meters
wave_thresholds = [1.5, 1.75, 2.0, 2.25, 2.5]              

sensitivity_rows = []
for wnd in wind_thresholds:
    for wav in wave_thresholds:
        # computes FDI for each threshold combination
        fdi_temp = ((df_master["max_wind_ms"] <= wnd) &
                    (df_master["max_swh_m"]   <= wav)).astype(int)
        sensitivity_rows.append({
            "wind_thresh_ms": wnd,
            # converted to knots for reference
            "wind_thresh_kt": round(wnd * MS_TO_KNOTS, 3),
            "wave_thresh_m":  wav,
            "fishable_days":  fdi_temp.sum(),
            "pct_fishable":   round(fdi_temp.mean() * 100, 2)
        })

# Collect all combinations into a new panddas dataframe
df_sensitivity = pd.DataFrame(sensitivity_rows)                                        
print("\nThreshold Sensitivity Analysis:")
print(df_sensitivity.to_string(index=False))
Total fishable days: 8278
Total unfishable days: 6329
% fishable: 56.67

Validation Summary (Sundays excluded):
       fdi  mean_csl_trips_per_1k  mean_csl_lbs_per_1k  pct_days_active
Unfishable               0.038558             1.400545        56.774075
  Fishable               0.044012             1.586591        62.515937

Threshold Sensitivity Analysis:
 wind_thresh_ms  wind_thresh_kt  wave_thresh_m  fishable_days  pct_fishable
            7.5          14.579           1.50           4872         33.35
            7.5          14.579           1.75           5048         34.56
            7.5          14.579           2.00           5087         34.83
            7.5          14.579           2.25           5094         34.87
            7.5          14.579           2.50           5099         34.91
            8.5          16.523           1.50           7697         52.69
            8.5          16.523           1.75           8278         56.67
            8.5          16.523           2.00           8372         57.31
            8.5          16.523           2.25           8389         57.43
            8.5          16.523           2.50           8395         57.47
           10.0          19.438           1.50           9143         62.59
           10.0          19.438           1.75          11707         80.15
           10.0          19.438           2.00          12505         85.61
           10.0          19.438           2.25          12620         86.40
           10.0          19.438           2.50          12636         86.51
           11.5          22.354           1.50           9151         62.65
           11.5          22.354           1.75          11929         81.67
           11.5          22.354           2.00          13377         91.58
           11.5          22.354           2.25          14053         96.21
           11.5          22.354           2.50          14229         97.41
           13.0          25.270           1.50           9151         62.65
           13.0          25.270           1.75          11930         81.67
           13.0          25.270           2.00          13390         91.67
           13.0          25.270           2.25          14101         96.54
           13.0          25.270           2.50          14384         98.47
# Subplot 1: FDI daily time series (annual rolling average for readability)
df_master["fdi_rolling"] = (
  df_master["fdi"].rolling(window=365, min_periods=180).mean() * 365
  ) # rolling annual fishable days
fig, ax = plt.subplots(figsize=(14, 4))
ax.plot(df_master["date"], df_master["fdi_rolling"], 
        color="steelblue", linewidth=1.2)
ax.set_title("Rolling Annual Fishable Days (365-day window)")
ax.set_xlabel("Date")
ax.set_ylabel("Fishable Days per Year")
# Clean integer y axis labels
ax.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.0f"))
plt.tight_layout()
plt.show()

# Subplot 2: validation bar chart
fig, axes = plt.subplots(1, 2, figsize=(10, 4))

axes[0].bar(validation_summary["fdi"], 
            validation_summary["mean_csl_trips_per_1k"],
            color=["coral", "steelblue"], edgecolor="black")
axes[0].set_title("Mean Lobster Trips per 1k\nFishable vs Unfishable Days")
axes[0].set_ylabel("Trips per 1,000 People")
axes[0].set_xlabel("")

axes[1].bar(validation_summary["fdi"], validation_summary["pct_days_active"],
            color=["coral", "steelblue"], edgecolor="black")
axes[1].set_title(
  "% Days With Any Lobster Activity\nFishable vs Unfishable Days"
  )
axes[1].set_ylabel("% of Days Active")
axes[1].set_xlabel("")

plt.tight_layout()
plt.show()

# Subplot3: sensitivity heatmap: reshape for heatmap format
df_heatmap = df_sensitivity.pivot(index="wave_thresh_m",
                                   columns="wind_thresh_ms",
                                   values="pct_fishable")
fig, ax = plt.subplots(figsize=(8, 5))

# Annotated heatmap of threshold combinations
sns.heatmap(df_heatmap, annot=True, fmt=".1f", cmap="YlGnBu",
            ax=ax, cbar_kws={"label": "% Fishable Days"})        
ax.set_title(
  "Threshold Sensitivity: % Fishable Days\n(Wind m/s vs Wave Height m)"
)
ax.set_xlabel("Wind Threshold (m/s)")
ax.set_ylabel("Wave Threshold (m)")
plt.tight_layout()
plt.show()

D.10 Summary of Seasonal FDI

# Aggregated daily FDI to the annual fishable day count
df_seasonal = (
    df_master.groupby("year")
    .agg(
        # total days in each year
        total_days        = ("fdi", "count"),
        # count of FDI = 1 days
        fishable_days     = ("fdi", "sum"),
        # count of FDI = 0 days
        unfishable_days   = ("fdi", lambda x: (x == 0).sum()),
        # % of year that was fishable
        pct_fishable      = ("fdi", lambda x: round(x.mean() * 100, 2)),
        # annual mean of daily max wind
        mean_wind_ms      = ("max_wind_ms", "mean"),
        # annual mean of daily max wave height
        mean_wave_m       = ("max_swh_m",   "mean"),
        # days with any lobster trips reported
        active_fishing_days = ("n_csl_trips", lambda x: (x > 0).sum()) 
    )
    .reset_index()
)
# Added lobster activity rate on fishable vs all days
fishable_active = (
    df_master[df_master["fdi"] == 1]
    .groupby("year")
     # lobster active days that were also fishable
    .agg(fishable_active_days = ("n_csl_trips", lambda x: (x > 0).sum())) 
    .reset_index()
)
# Join back onto seasonal summary
df_seasonal = df_seasonal.merge(fishable_active, on="year", how="left")     

# Mean annual fishable days across full period
long_run_mean = df_seasonal["fishable_days"].mean() 

# Standard deviation of annual fishable days
long_run_std  = df_seasonal["fishable_days"].std() 

# Calculate the long run mean and strike level reference
strike_90     = long_run_mean * 0.90 # 90% of mean as indicative strike level
strike_80     = long_run_mean * 0.80 # 80% of mean as indicative strike level
print(f"Long run mean fishable days:      {round(long_run_mean, 1)}")
print(f"Long run std dev:                 {round(long_run_std, 1)}")
print(f"Indicative strike at 90% of mean: {round(strike_90, 1)} days")
print(f"Indicative strike at 80% of mean: {round(strike_80, 1)} days")
print(
  f"\nYears below 90% strike: {(
    df_seasonal['fishable_days'] < strike_90).sum()}"
    )
print(
  f"Years below 80% strike: {(
    df_seasonal['fishable_days'] < strike_80).sum()}"
    )
print(f"\nSeasonal FDI Summary:")
print(df_seasonal[["year", "fishable_days", "pct_fishable",
                    "mean_wind_ms", "mean_wave_m"]].to_string(index=False))
Long run mean fishable days:      207.0
Long run std dev:                 23.9
Indicative strike at 90% of mean: 186.3 days
Indicative strike at 80% of mean: 165.6 days

Years below 90% strike: 8
Years below 80% strike: 1

Seasonal FDI Summary:
 year  fishable_days  pct_fishable  mean_wind_ms  mean_wave_m
 1985            175         47.95      8.378711     1.402619
 1986            189         51.78      8.209939     1.384351
 1987            217         59.45      7.966806     1.337351
 1988            161         43.99      8.540703     1.487091
 1989            192         52.60      8.358651     1.426431
 1990            174         47.67      8.393137     1.438188
 1991            175         47.95      8.457029     1.460763
 1992            206         56.28      7.966727     1.447969
 1993            207         56.71      8.077101     1.442734
 1994            193         52.88      8.212212     1.450661
 1995            253         69.32      7.695674     1.405975
 1996            192         52.46      8.383011     1.537843
 1997            190         52.05      8.382274     1.458454
 1998            187         51.23      8.385344     1.512378
 1999            227         62.19      8.088638     1.471468
 2000            185         50.55      8.306767     1.489493
 2001            210         57.53      8.071573     1.427403
 2002            208         56.99      8.196841     1.437431
 2003            220         60.27      7.847460     1.404625
 2004            200         54.64      8.166434     1.444430
 2005            256         70.14      7.572696     1.339890
 2006            231         63.29      7.885105     1.406349
 2007            246         67.40      7.798133     1.358193
 2008            199         54.37      8.245858     1.478545
 2009            209         57.26      8.053282     1.449005
 2010            224         61.37      7.788298     1.414063
 2011            219         60.00      7.868040     1.413033
 2012            223         60.93      7.945315     1.402381
 2013            182         49.86      8.323493     1.485063
 2014            219         60.00      8.017828     1.416118
 2015            204         55.89      8.091917     1.441943
 2016            238         65.03      7.798298     1.420609
 2017            222         60.82      8.099177     1.494537
 2018            176         48.22      8.393065     1.566984
 2019            230         63.01      7.750875     1.382264
 2020            219         59.84      7.931521     1.425647
 2021            172         47.12      8.388025     1.455910
 2022            187         51.23      8.345445     1.485085
 2023            214         58.63      7.832936     1.410504
 2024            247         68.04      7.617995     1.333671
# Sublot 1: annual fishable days time series with 80/90 mean strike levels
fig, ax = plt.subplots(figsize=(14, 5))

# annual bar chart
ax.bar(df_seasonal["year"], df_seasonal["fishable_days"],
       color="steelblue", edgecolor="black", alpha=0.7, label="Fishable Days")       

# mean ref. line
ax.axhline(long_run_mean, color="green",  linewidth=1.5, linestyle="--",
           label=f"Long Run Mean ({round(long_run_mean, 1)} days)")                 
           
# 90% strike ref. line
ax.axhline(strike_90,     color="orange", linewidth=1.5, linestyle="--",
           label=f"90% Strike ({round(strike_90, 1)} days)")                         
# 80% strike ref. line           
ax.axhline(strike_80,     color="red",    linewidth=1.5, linestyle="--",
           label=f"80% Strike ({round(strike_80, 1)} days)")                         
           
ax.set_title(
  "Annual Fishable Days Index (FDI) — St. Croix Lobster Fishery 1985–2024"
  )
ax.set_xlabel("Year")
ax.set_ylabel("Fishable Days")
ax.legend(loc="lower left")
ax.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.0f"))
plt.tight_layout()
plt.show()

# Subplot 2: FDI vs active lobster fishing days per year
fig, ax = plt.subplots(figsize=(14, 5))
ax.plot(df_seasonal["year"], df_seasonal["fishable_days"],
        color="steelblue", linewidth=2, marker="o", markersize=4, 
        label="Fishable Days (FDI)")
ax.plot(df_seasonal["year"], df_seasonal["active_fishing_days"],
        color="coral", linewidth=2, marker="o", markersize=4, 
        label="Days With Lobster Activity Reported")
ax.set_title(
  "Annual FDI vs Reported Lobster Activity Days — St. Croix 1985–2024"
  )
ax.set_xlabel("Year")
ax.set_ylabel("Days per Year")
ax.legend()
plt.tight_layout()
plt.grid(True,alpha=0.5)
plt.show()

# Subplot 3: distribution of annual fishable days
fig, ax = plt.subplots(figsize=(8, 4))

# histogram of annual FDI values
ax.hist(df_seasonal["fishable_days"], bins=15, color="steelblue",
        edgecolor="black", alpha=0.8)
ax.axvline(long_run_mean, color="green",  linestyle="--", linewidth=1.5,
           label=f"Mean ({round(long_run_mean, 1)})")
ax.axvline(strike_90,     color="orange", linestyle="--", linewidth=1.5,
           label=f"90% Strike ({round(strike_90, 1)})")
ax.axvline(strike_80,     color="red",    linestyle="--", linewidth=1.5,
           label=f"80% Strike ({round(strike_80, 1)})")
ax.set_title("Distribution of Annual Fishable Days — St. Croix 1985–2024")
ax.set_xlabel("Fishable Days per Year")
ax.set_ylabel("Frequency (Years)")
ax.legend()
plt.tight_layout()
plt.show()

D.11 Exporting the Outputs

# Export the full daily master table to 2 .csv files
master_path   = "../data/fdi_master_daily.csv"
seasonal_path = "../data/fdi_seasonal_summary.csv"

# save full daily table, no row index
df_master.to_csv(master_path, index=False)  

# save annual FDI summary, no row index
df_seasonal.to_csv(seasonal_path, index=False)  
# Checks
print(f"Daily master table exported:    {master_path}")
print(f"  Rows: {len(df_master)}, Columns: {len(df_master.columns)}")
print(f"\nSeasonal FDI summary exported:  {seasonal_path}")
print(f"  Rows: {len(df_seasonal)}, Columns: {len(df_seasonal.columns)}")

# Printout of final columns header of both .csv files: list out every column
print(f"\nDaily master columns:")
for col in df_master.columns:
    print(f"  {col}")

print(f"\nSeasonal summary columns:")
for col in df_seasonal.columns:
    print(f"  {col}")
Daily master table exported:    ../data/fdi_master_daily.csv
  Rows: 14607, Columns: 23

Seasonal FDI summary exported:  ../data/fdi_seasonal_summary.csv
  Rows: 40, Columns: 9

Daily master columns:
  date
  year
  month
  weekday
  n_trips
  n_csl_trips
  all_all
  csl_all
  max_wind_ms
  max_wind_kt
  max_gust_ms
  max_gust_kt
  max_swh_m
  population
  csl_trips_per_1k
  all_trips_per_1k
  csl_lbs_per_1k
  all_lbs_per_1k
  is_sunday
  wind_ok
  wave_ok
  fdi
  fdi_rolling

Seasonal summary columns:
  year
  total_days
  fishable_days
  unfishable_days
  pct_fishable
  mean_wind_ms
  mean_wave_m
  active_fishing_days
  fishable_active_days