data-matching · git:20260424.d50cf5a · 2026-04-24 · sha256 e707bfce746df325

data-matching git:20260424.d50cf5aA

Immutable. This exact content never changes and is served at /api/v1/blob/e707bfce746df325.

---
name: data-matching
description: Matching observation data to simulation output with exact datetime and depth binning
---

# Data Matching Skill

## Overview
Successfully matching observations to simulations requires careful handling of datetime and depth coordinates. The matching must use exact values with proper rounding—no interpolation or nearest-neighbor approximation.

## Observation Data Format

CSV with columns:
```
datetime,depth,temp,OXY_oxy
2009-01-21 12:00:00,0,0.1,16.3
2009-01-21 12:00:00,1,0.7,16.3
...
```

- **datetime**: ISO format timestamp
- **depth**: Measured depth in meters
- **temp**: Water temperature in °C
- **OXY_oxy**: Oxygen (not used for temperature RMSE)

## Simulation Output Characteristics

GLM output:
- **Time dimension**: Regular hourly intervals from simulation start
- **Depth dimension**: Variable number of layers based on model dynamics
- **Temperature**: Simulated at each time step and depth layer

## Exact Matching Algorithm

### Step 1: Load Observations
```python
import pandas as pd

obs_df = pd.read_csv('/root/field_temp_oxy.csv')
obs_df['datetime'] = pd.to_datetime(obs_df['datetime'])
```

### Step 2: Round Depths
Round observation depths to nearest meter (standard practice):
```python
obs_df['depth_rounded'] = obs_df['depth'].round(0)
```

### Step 3: Extract Simulation Data
```python
import netCDF4 as nc
from netCDF4 import num2date

ds = nc.Dataset('/root/output/output.nc')
temp_sim = ds.variables['temp'][:]      # [time, depth]
z_sim = ds.variables['z'][:]            # depth coordinates
time_sim = ds.variables['time'][:]      # time values

# Convert time to datetime
time_var = ds.variables['time']
dates_sim = num2date(time_sim, time_var.units)

ds.close()
```

### Step 4: Exact Matching
```python
def exact_match(obs_df, temp_sim, z_sim, dates_sim):
    """
    Match observations to simulation using exact datetime and rounded-depth

    Returns: aligned arrays of simulated temps, observed temps,
             and metadata for filtering
    """
    import numpy as np

    matched = {
        'sim_temp': [],
        'obs_temp': [],
        'depth': [],
        'datetime': [],
        'obs_idx': []
    }

    for idx, row in obs_df.iterrows():
        obs_date = row['datetime']
        obs_depth = row['depth_rounded']
        obs_temp = row['temp']

        # Find time index: exact datetime match
        time_idx = None
        for i, sim_date in enumerate(dates_sim):
            if sim_date == obs_date:
                time_idx = i
                break

        if time_idx is None:
            continue  # No exact datetime match

        # Find depth index: exact depth match
        depth_idx = None
        for j, sim_z in enumerate(z_sim):
            if np.isclose(sim_z, obs_depth, atol=0.01):
                depth_idx = j
                break

        if depth_idx is None:
            continue  # No exact depth match

        # Record match
        matched['sim_temp'].append(temp_sim[time_idx, depth_idx])
        matched['obs_temp'].append(obs_temp)
        matched['depth'].append(obs_depth)
        matched['datetime'].append(obs_date)
        matched['obs_idx'].append(idx)

    return matched
```

## Quality Checks

### Missing Matches
```python
# Check what percentage of observations were matched
total_obs = len(obs_df)
matched_count = len(matched['sim_temp'])
match_fraction = matched_count / total_obs
print(f"Matched {matched_count}/{total_obs} observations ({100*match_fraction:.1f}%)")
```

### Temporal Coverage
```python
# Check date range of matches
import pandas as pd
match_dates = pd.DataFrame(matched['datetime'])
print(f"Match date range: {match_dates.min()} to {match_dates.max()}")
```

### Depth Distribution
```python
# Check which depths are represented
import numpy as np
matched_depths = np.array(matched['depth'])
unique_depths = np.unique(matched_depths)
print(f"Matched depths: {sorted(unique_depths)}")
```

## Filtering for Metrics

After exact matching, apply semantic filters:

```python
import numpy as np

matched_sim = np.array(matched['sim_temp'])
matched_obs = np.array(matched['obs_temp'])
matched_depths = np.array(matched['depth'])
matched_dates = np.array(matched['datetime'])

# Overall RMSE: all matches
overall_mask = np.ones(len(matched_sim), dtype=bool)

# Annual deep (depths >= 13m)
annual_deep_mask = matched_depths >= 13

# Summer deep (June-Sept, depths >= 13m)
summer_mask = np.array([d.month in [6, 7, 8, 9]
                        for d in matched_dates])
summer_deep_mask = summer_mask & (matched_depths >= 13)

# Calculate RMSE for each category
from numpy import sqrt, mean
overall_rmse = sqrt(mean((matched_sim - matched_obs)**2))
annual_deep_rmse = sqrt(mean((matched_sim[annual_deep_mask] -
                              matched_obs[annual_deep_mask])**2))
summer_deep_rmse = sqrt(mean((matched_sim[summer_deep_mask] -
                              matched_obs[summer_deep_mask])**2))
```

## Common Pitfalls

1. **Rounding inconsistency**: Always round observation depths the same way
2. **Timezone issues**: Ensure times are in same timezone before comparison
3. **Nearest-neighbor fallback**: Must use exact matches only
4. **Interpolation**: Do NOT interpolate simulation to observation depths/times