| name | metocean-statistics |
| description | Statistical analysis of metocean data including extreme value analysis, return periods, joint probability distributions, and directional statistics. Use for design criteria, fatigue analysis, and operational limits. |
Metocean Statistics Skill
When to Use
Trigger this skill when the user asks about:
Extreme Value Analysis
- "Calculate extreme wave heights"
- "Return period analysis"
- "Fit GEV distribution"
- "100-year return period"
- "Peak-over-threshold analysis"
- "Annual maxima statistics"
Distribution Fitting
- "Joint probability distribution"
- "Hs-Tp joint distribution"
- "Environmental contours"
- "IFORM contours"
- "Fit distribution to wave data"
Temporal Statistics
- "Monthly statistics"
- "Seasonal trends"
- "Annual variability"
- "Exceedance duration"
Directional Analysis
- "Wave rose"
- "Wind rose"
- "Directional statistics"
- "Sector-based analysis"
Design Applications
- "Design criteria"
- "Fatigue analysis"
- "Operational limits"
- "Weather windows"
Analysis Types
Temporal Statistics
Monthly Aggregations
- Mean, max, min, standard deviation
- Percentiles (P50, P90, P99)
- Sample count and data availability
Seasonal Aggregations
- Winter (DJF), Spring (MAM), Summer (JJA), Autumn (SON)
- Seasonal extremes
- Inter-annual variability
Annual Trends
- Annual mean and max time series
- Trend analysis (linear regression)
- Climate variability indicators
Exceedance Duration
- Persistence above threshold
- Storm duration statistics
- Operational weather windows
Extreme Value Analysis
Block Maxima Method
- Annual maxima extraction
- Monthly maxima for shorter records
- Requires minimum 10 years for reliability
- Standard approach for design criteria
Peak-Over-Threshold (POT)
- Extract independent peaks above threshold
- Suitable for shorter records (3+ years)
- More efficient use of data
- Requires declustering of dependent events
GEV Distribution (Generalized Extreme Value)
- Three-parameter distribution (shape, location, scale)
- Encompasses Gumbel, Frechet, Weibull types
- Standard for block maxima analysis
- Shape parameter indicates tail behavior
GPD Distribution (Generalized Pareto)
- Two-parameter distribution for exceedances
- Paired with POT method
- Threshold selection critical
- More flexible for tail modeling
Return Periods
Standard Return Periods
| Period | Application |
|---|
| 1-year | Annual operating design |
| 2-year | Seasonal planning |
| 5-year | Short-term structure design |
| 10-year | Installation operations |
| 25-year | Platform design basis |
| 50-year | Structure design life |
| 100-year | Ultimate limit state |
| 500-year | Abnormal/survival conditions |
Confidence Intervals
- Profile likelihood method
- Bootstrap resampling
- Delta method approximation
- Typically report 95% CI
Joint Probability
Hs-Tp Joint Distribution
- Wave height and period correlation
- Scatter diagram representation
- Steepness limits (physical constraints)
- Critical for dynamic analysis
Wind-Wave Correlation
- Wind speed vs wave height
- Lag correlation analysis
- Sea state development
- Combined loading analysis
Environmental Contours
- IFORM method (Inverse First Order Reliability Method)
- Direct sampling approach
- Joint exceedance probability
- Design envelope definition
Directional Analysis
Wave Roses
- Direction vs magnitude distribution
- Sector occurrence frequency
- Mean and maximum by sector
- Seasonal directional variation
Wind Roses
- Similar to wave roses
- Different directional convention (from vs to)
- Fetch-limited conditions
- Dominant wind patterns
Sector-Based Statistics
- 8, 12, or 16 sector divisions
- Exceedance by direction
- Design wave by sector
- Directional spreading
External Tool Integration
metocean-stats Package
Installation:
pip install metocean-stats
Extreme Value Analysis:
from metocean_stats import EVA
eva = EVA(data=wave_heights, method='block_maxima', block_size='year')
eva.fit('GEV')
rp_10yr = eva.return_level(10)
rp_100yr = eva.return_level(100)
rp_100yr_ci = eva.return_level_ci(100, alpha=0.05)
eva.plot_return_levels()
eva.plot_qq()
eva.plot_probability()
Peak-Over-Threshold:
from metocean_stats import EVA
eva_pot = EVA(
data=wave_heights,
method='peak_over_threshold',
threshold=np.percentile(wave_heights, 95),
decluster_time='48h'
)
eva_pot.fit('GPD')
rp_values = eva_pot.return_level([1, 10, 50, 100])
Return Period Tables:
from metocean_stats import return_periods
rp_table = return_periods.calculate(
data=df,
parameters=['Hs', 'Tp', 'U10'],
return_periods=[1, 2, 5, 10, 25, 50, 100],
method='GEV',
confidence_level=0.95
)
rp_table.to_csv('return_periods.csv')
Joint Probability Analysis:
from metocean_stats import joint_probability
jp = joint_probability.fit(
df['Hs'],
df['Tp'],
method='kernel',
bandwidth='scott'
)
jp.plot_contours(levels=[0.1, 0.5, 0.9])
jp.plot_scatter_diagram(hs_bins=20, tp_bins=20)
Environmental Contours:
from metocean_stats.contours import IFORM, DirectSampling
iform = IFORM(df['Hs'], df['Tp'])
contour_100yr = iform.contour(return_period=100, n_points=100)
iform.plot()
ds = DirectSampling(df['Hs'], df['Tp'], n_samples=10000)
contour_ds = ds.contour(return_period=100)
ds.plot()
Directional Statistics:
from metocean_stats import directional
dir_stats = directional.sector_stats(
speeds=df['Hs'],
directions=df['wave_dir'],
sectors=16,
statistics=['mean', 'max', 'p90', 'count']
)
directional.plot_rose(
speeds=df['Hs'],
directions=df['wave_dir'],
bins=[0, 1, 2, 3, 4, 5],
title='Significant Wave Height Rose'
)
dir_extremes = directional.sector_extremes(
speeds=df['Hs'],
directions=df['wave_dir'],
sectors=8,
return_periods=[1, 10, 50, 100]
)
scipy.stats Integration
GEV Distribution Fitting:
from scipy import stats
import numpy as np
annual_max = df.groupby(df['time'].dt.year)['Hs'].max().values
shape, loc, scale = stats.genextreme.fit(annual_max)
def return_level(T, shape, loc, scale):
"""Calculate return level for return period T years."""
p = 1 - 1/T
return stats.genextreme.ppf(p, shape, loc=loc, scale=scale)
return_periods = [1, 2, 5, 10, 25, 50, 100, 500]
return_levels = {T: return_level(T, shape, loc, scale) for T in return_periods}
ks_stat, p_value = stats.kstest(annual_max, 'genextreme', args=(shape, loc, scale))
GPD Distribution for POT:
from scipy import stats
import numpy as np
threshold = np.percentile(wave_heights, 95)
exceedances = wave_heights[wave_heights > threshold] - threshold
shape_gpd, loc_gpd, scale_gpd = stats.genpareto.fit(exceedances, floc=0)
def pot_return_level(T, threshold, shape, scale, rate):
"""Return level from POT analysis."""
m = T * rate
return threshold + scale / shape * ((m) ** shape - 1)
n_years = (df['time'].max() - df['time'].min()).days / 365.25
rate = len(exceedances) / n_years
QQ Plot Diagnostics:
import matplotlib.pyplot as plt
from scipy import stats
fig, ax = plt.subplots(figsize=(8, 6))
stats.probplot(annual_max, dist=stats.genextreme, sparams=(shape, loc, scale), plot=ax)
ax.set_title('GEV Q-Q Plot')
plt.savefig('qq_plot.png')
NREL Floating Wind Methodology
Site Conditions Assessment
The NREL floating wind methodology provides a standardized approach for
characterizing offshore site conditions for floating wind turbine design.
Return Period Table Format:
| Return Period | Hs (m) | Tp (s) | U10 (m/s) | Current (m/s) |
|---|
| 1-year | 5.2 | 10.5 | 18.3 | 0.45 |
| 10-year | 7.8 | 12.1 | 23.4 | 0.62 |
| 50-year | 9.4 | 13.2 | 27.1 | 0.78 |
| 100-year | 10.2 | 13.8 | 28.9 | 0.85 |
| 500-year | 12.1 | 15.1 | 32.4 | 1.02 |
Design Load Cases:
- DLC 1.6: Normal operation with extreme waves
- DLC 6.1: Parked, extreme conditions (50-year)
- DLC 6.2: Parked, extreme conditions (50-year) with fault
Joint Distribution Requirements:
- Hs-Tp scatter diagram (minimum 10 years)
- Environmental contours for DLC validation
- Conditional Tp given Hs for response analysis
Environmental Contour Methods
IFORM Method:
- Transform to standard normal space
- Apply reliability index for target probability
- Inverse transform to physical space
- Conservative for design applications
Direct Sampling:
- Monte Carlo simulation in physical space
- Empirical exceedance probability
- Better captures complex dependencies
- Computationally intensive
Python Workflow Examples
Complete Analysis Pipeline
import pandas as pd
import numpy as np
from scipy import stats
from datetime import date
async def run_extreme_analysis(station_id: str, start_year: int, end_year: int):
"""Complete extreme value analysis workflow."""
from worldenergydata.metocean.clients.ndbc_client import NDBCClient
from worldenergydata.metocean.processors.data_harmonizer import DataHarmonizer
async with NDBCClient() as client:
data = await client.fetch_historical(
station_id=station_id,
start_date=date(start_year, 1, 1),
end_date=date(end_year, 12, 31)
)
harmonizer = DataHarmonizer()
records = []
for obs in data.data:
record = harmonizer.harmonize_ndbc(obs, data.latitude, data.longitude)
records.append(record)
df = pd.DataFrame(records)
df['time'] = pd.to_datetime(df['observation_time'])
annual_max = df.groupby(df['time'].dt.year)['wave_height_m'].max()
shape, loc, scale = stats.genextreme.fit(annual_max.values)
return_periods = [1, 2, 5, 10, 25, 50, 100, 500]
results = []
for T in return_periods:
p = 1 - 1/T
rl = stats.genextreme.ppf(p, shape, loc=loc, scale=scale)
n_bootstrap = 1000
bootstrap_rl = []
for _ in range(n_bootstrap):
sample = np.random.choice(annual_max.values, size=len(annual_max), replace=True)
s, l, sc = stats.genextreme.fit(sample)
bootstrap_rl.append(stats.genextreme.ppf(p, s, loc=l, scale=sc))
ci_lower = np.percentile(bootstrap_rl, 2.5)
ci_upper = np.percentile(bootstrap_rl, 97.5)
results.append({
'return_period_years': T,
'Hs_m': round(rl, 2),
'Hs_lower_CI': round(ci_lower, 2),
'Hs_upper_CI': round(ci_upper, 2)
})
return pd.DataFrame(results)
Monthly Statistics Calculation
def calculate_monthly_stats(df: pd.DataFrame) -> pd.DataFrame:
"""Calculate comprehensive monthly statistics for metocean parameters."""
df = df.copy()
df['month'] = pd.to_datetime(df['observation_time']).dt.month
agg_funcs = {
'wave_height_m': ['mean', 'max', 'min', 'std', 'count',
lambda x: x.quantile(0.5),
lambda x: x.quantile(0.9),
lambda x: x.quantile(0.99)],
'wave_period_s': ['mean', 'max', 'std'],
'wind_speed_ms': ['mean', 'max', 'std']
}
monthly = df.groupby('month').agg(agg_funcs)
monthly.columns = ['_'.join(col).strip() for col in monthly.columns.values]
month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
monthly.index = [month_names[i-1] for i in monthly.index]
return monthly
Seasonal Statistics
def calculate_seasonal_stats(df: pd.DataFrame) -> pd.DataFrame:
"""Calculate seasonal statistics for metocean parameters."""
df = df.copy()
df['time'] = pd.to_datetime(df['observation_time'])
df['month'] = df['time'].dt.month
season_map = {12: 'Winter', 1: 'Winter', 2: 'Winter',
3: 'Spring', 4: 'Spring', 5: 'Spring',
6: 'Summer', 7: 'Summer', 8: 'Summer',
9: 'Autumn', 10: 'Autumn', 11: 'Autumn'}
df['season'] = df['month'].map(season_map)
seasonal = df.groupby('season').agg({
'wave_height_m': ['mean', 'max', 'std', 'count'],
'wind_speed_ms': ['mean', 'max', 'std']
})
season_order = ['Winter', 'Spring', 'Summer', 'Autumn']
seasonal = seasonal.reindex(season_order)
return seasonal
Directional Analysis
def calculate_directional_stats(
df: pd.DataFrame,
speed_col: str = 'wave_height_m',
dir_col: str = 'wave_direction_deg',
sectors: int = 16
) -> dict:
"""Calculate statistics by directional sector."""
df = df.copy()
sector_width = 360 / sectors
df['sector'] = ((df[dir_col] + sector_width/2) / sector_width).astype(int) % sectors
df['sector_center'] = df['sector'] * sector_width
stats_by_sector = {}
total_count = len(df.dropna(subset=[speed_col, dir_col]))
for sector in range(sectors):
sector_center = sector * sector_width
sector_data = df[df['sector'] == sector][speed_col].dropna()
if len(sector_data) > 0:
stats_by_sector[sector_center] = {
'direction_deg': sector_center,
'direction_label': _get_direction_label(sector_center),
'mean': round(sector_data.mean(), 2),
'max': round(sector_data.max(), 2),
'std': round(sector_data.std(), 2),
'p90': round(sector_data.quantile(0.9), 2),
'count': len(sector_data),
'occurrence_pct': round(len(sector_data) / total_count * 100, 1)
}
else:
stats_by_sector[sector_center] = {
'direction_deg': sector_center,
'direction_label': _get_direction_label(sector_center),
'mean': None, 'max': None, 'std': None, 'p90': None,
'count': 0, 'occurrence_pct': 0
}
return stats_by_sector
def _get_direction_label(degrees: float) -> str:
"""Convert degrees to compass direction label."""
directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']
index = int((degrees + 11.25) / 22.5) % 16
return directions[index]
Joint Probability Distribution
def calculate_joint_distribution(
df: pd.DataFrame,
var1: str = 'wave_height_m',
var2: str = 'wave_period_s',
bins1: int = 20,
bins2: int = 20
) -> pd.DataFrame:
"""Calculate Hs-Tp joint distribution (scatter diagram)."""
data = df[[var1, var2]].dropna()
var1_edges = np.linspace(data[var1].min(), data[var1].max(), bins1 + 1)
var2_edges = np.linspace(data[var2].min(), data[var2].max(), bins2 + 1)
hist, _, _ = np.histogram2d(
data[var1], data[var2],
bins=[var1_edges, var2_edges]
)
prob = hist / hist.sum()
var1_centers = (var1_edges[:-1] + var1_edges[1:]) / 2
var2_centers = (var2_edges[:-1] + var2_edges[1:]) / 2
scatter = pd.DataFrame(
prob,
index=np.round(var1_centers, 2),
columns=np.round(var2_centers, 2)
)
return scatter
Exceedance Duration Analysis
def calculate_exceedance_duration(
df: pd.DataFrame,
parameter: str = 'wave_height_m',
threshold: float = 2.5,
time_col: str = 'observation_time'
) -> dict:
"""Calculate exceedance duration statistics."""
df = df.copy()
df['time'] = pd.to_datetime(df[time_col])
df = df.sort_values('time')
df['exceeds'] = df[parameter] > threshold
df['event_id'] = (df['exceeds'] != df['exceeds'].shift()).cumsum()
events = df[df['exceeds']].groupby('event_id').agg({
'time': ['min', 'max', 'count']
})
events.columns = ['start', 'end', 'observations']
events['duration_hours'] = (events['end'] - events['start']).dt.total_seconds() / 3600
total_hours = (df['time'].max() - df['time'].min()).total_seconds() / 3600
exceedance_hours = events['duration_hours'].sum()
return {
'threshold': threshold,
'n_events': len(events),
'total_exceedance_hours': round(exceedance_hours, 1),
'exceedance_percentage': round(exceedance_hours / total_hours * 100, 2),
'mean_duration_hours': round(events['duration_hours'].mean(), 1),
'max_duration_hours': round(events['duration_hours'].max(), 1),
'min_duration_hours': round(events['duration_hours'].min(), 1)
}
YAML Configuration Templates
Extreme Value Analysis Configuration
analysis:
name: extreme_value_analysis
type: extreme_value
method: block_maxima
block_size: year
distribution: GEV
fitting_method: MLE
pot_settings:
threshold_percentile: 95
decluster_time_hours: 48
min_separation: 3
return_periods: [1, 2, 5, 10, 25, 50, 100, 500]
confidence:
level: 0.95
method: bootstrap
n_bootstrap: 1000
output:
format: csv
include_diagnostics: true
plot_return_levels: true
plot_qq: true
Joint Probability Configuration
joint_analysis:
name: hs_tp_joint_distribution
variables:
x: wave_height_m
y: wave_period_s
method: kernel
bandwidth: scott
scatter_diagram:
x_bins: 20
y_bins: 20
normalize: probability
contours:
method: IFORM
return_periods: [10, 50, 100]
n_points: 100
constraints:
max_steepness: 0.1
min_tp_factor: 3.0
Directional Analysis Configuration
directional_analysis:
name: wave_directional_stats
sectors: 16
convention: from
parameters:
- name: wave_height_m
statistics: [mean, max, p90, occurrence]
- name: wave_period_s
statistics: [mean, max]
rose_plot:
bins: [0, 1, 2, 3, 4, 5, 6]
colors: viridis
legend_title: "Hs (m)"
extremes:
method: sector_maxima
return_periods: [1, 10, 50, 100]
Monthly Statistics Configuration
temporal_analysis:
name: monthly_statistics
period: monthly
statistics:
- mean
- max
- min
- std
- p50
- p90
- p99
- count
parameters:
- wave_height_m
- wave_period_s
- wind_speed_ms
- wind_direction_deg
output:
format: csv
include_plots: true
plot_type: heatmap
Output Formats
Return Period Table (CSV)
return_period_years,Hs_m,Hs_lower_CI,Hs_upper_CI,Tp_s,Tp_lower_CI,Tp_upper_CI,U10_ms,U10_lower_CI,U10_upper_CI
1,5.2,4.8,5.6,10.5,9.8,11.2,18.3,16.9,19.7
2,6.1,5.6,6.6,11.2,10.4,12.0,20.5,18.9,22.1
5,7.1,6.5,7.7,11.8,10.9,12.7,22.1,20.3,23.9
10,7.8,7.1,8.5,12.1,11.2,13.0,23.4,21.4,25.4
25,8.7,7.9,9.5,12.6,11.6,13.6,25.2,23.0,27.4
50,9.4,8.4,10.4,13.2,12.1,14.3,27.1,24.7,29.5
100,10.2,9.0,11.4,13.8,12.6,15.0,28.9,26.2,31.6
500,12.1,10.5,13.7,15.1,13.7,16.5,32.4,29.2,35.6
Summary Statistics (JSON)
{
"parameter": "wave_height_m",
"unit": "m",
"period": {
"start": "2014-01-01",
"end": "2024-12-31",
"years": 11
},
"statistics": {
"mean": 1.45,
"std": 0.82,
"min": 0.1,
"max": 8.7,
"p50": 1.3,
"p90": 2.6,
"p99": 4.1,
"count": 96432,
"missing_pct": 2.3
},
"monthly": {
"Jan": {"mean": 2.1, "max": 7.2, "std": 1.1},
"Feb": {"mean": 2.0, "max": 6.8, "std": 1.0},
"...": "..."
},
"annual_maxima": {
"2014": 6.2,
"2015": 7.1,
"...": "..."
},
"gev_parameters": {
"shape": -0.12,
"location": 5.8,
"scale": 1.2
}
}
Directional Statistics (JSON)
{
"parameter": "wave_height_m",
"sectors": 8,
"convention": "direction_from",
"data": [
{"direction": 0, "label": "N", "mean": 1.2, "max": 5.4, "p90": 2.1, "occurrence_pct": 8.2},
{"direction": 45, "label": "NE", "mean": 1.4, "max": 6.1, "p90": 2.4, "occurrence_pct": 12.5},
{"direction": 90, "label": "E", "mean": 1.1, "max": 4.8, "p90": 1.9, "occurrence_pct": 9.8},
{"direction": 135, "label": "SE", "mean": 0.9, "max": 4.2, "p90": 1.6, "occurrence_pct": 7.1},
{"direction": 180, "label": "S", "mean": 1.3, "max": 5.8, "p90": 2.2, "occurrence_pct": 15.3},
{"direction": 225, "label": "SW", "mean": 1.8, "max": 8.7, "p90": 3.1, "occurrence_pct": 22.4},
{"direction": 270, "label": "W", "mean": 1.6, "max": 7.2, "p90": 2.8, "occurrence_pct": 16.2},
{"direction": 315, "label": "NW", "mean": 1.3, "max": 5.9, "p90": 2.3, "occurrence_pct": 8.5}
]
}
Best Practices
Data Requirements
Minimum Record Lengths:
- Block maxima (annual): Minimum 10 years recommended
- Peak-over-threshold: Minimum 3 years with high-frequency data
- Monthly statistics: Minimum 3 years for seasonal patterns
- Directional analysis: Minimum 1 year continuous data
Data Quality:
- Check for gaps and document missing periods
- Validate against neighboring stations
- Remove spurious values (instrument errors)
- Ensure consistent time zone and conventions
Distribution Fitting
GEV Fitting:
- Use maximum likelihood estimation (MLE)
- Check shape parameter range (-0.5 to 0.5 typical for ocean waves)
- Validate with QQ plots and probability plots
- Consider regional frequency analysis for short records
Threshold Selection for POT:
- Use mean residual life plot
- Test sensitivity to threshold choice
- Ensure independence of peaks (declustering)
- Typical threshold: 90th-95th percentile
Uncertainty Quantification
Always Report:
- Confidence intervals for return levels
- Sample size and data availability
- Distribution fit quality metrics
- Assumptions and limitations
Validation Methods:
- Split-sample testing
- Cross-validation with nearby stations
- Comparison with historical extremes
- Sensitivity analysis
Design Applications
Conservative Approach:
- Use upper confidence interval for design
- Consider climate change trends
- Account for measurement uncertainty
- Apply appropriate safety factors
Common Workflows
1. Design Criteria Development
Step 1: Data Collection
- Fetch 10+ years of hourly data
- Check data quality and gaps
- Document data source and period
Step 2: Annual Maxima Extraction
- Extract annual maximum for each year
- Check for outliers
- Ensure independence
Step 3: Distribution Fitting
- Fit GEV distribution
- Validate fit with diagnostics
- Check shape parameter
Step 4: Return Period Calculation
- Calculate return levels (1-500 year)
- Compute confidence intervals
- Create return period table
Step 5: Export Results
- CSV table for engineering use
- JSON for database storage
- Plots for reports
2. Fatigue Analysis Support
Step 1: Load Time Series
- Hourly Hs-Tp pairs
- Minimum 3 years data
Step 2: Joint Distribution
- Calculate scatter diagram
- Fit joint distribution
- Define Hs-Tp bins
Step 3: Occurrence Matrix
- Calculate occurrence hours per bin
- Normalize to annual basis
- Export for fatigue tools
Step 4: Validate
- Check physical constraints
- Compare with design basis
- Document assumptions
3. Operational Weather Windows
Step 1: Define Operational Limits
- Maximum Hs for operation
- Maximum wind speed
- Minimum visibility
Step 2: Monthly Exceedance
- Calculate monthly exceedance %
- Identify seasonal patterns
- Calculate weather window statistics
Step 3: Persistence Analysis
- Duration above threshold
- Duration below threshold
- Waiting time statistics
Step 4: Planning Output
- Monthly operability table
- Seasonal summary
- Risk assessment input