Compound Condition Detection: How to Combine Multiple API Fields to Catch What Single Fields Miss

A condition code of 1135 means fog. A visibility reading of 0.3 km also suggests fog. But if you’re only checking one of those, you’re going to miss states that are dangerous in practice but don’t trip either threshold on their own.

This is the compound condition problem. Individual fields in a weather API response are designed to answer one question each. The real world rarely asks just one question at a time. A field sitting at 85% relative humidity doesn’t tell you much. A field sitting at 85% humidity with a dew point spread of 0.8°C, visibility at 2.1 km, and wind speed under 3 kph tells you something is forming — or has already formed — and your fog detection is about to miss it because vis_km hasn’t dropped to your threshold yet.

Why Single-Field Thresholds Fail in the Middle Cases

The obvious approach is to pick a threshold and gate on it. Visibility below 1 km? Flag fog. Temperature below 0°C? Flag ice risk. Wind above 50 kph? Flag storm. This works well at the extremes. It breaks down in the transitions — which is exactly where the operationally interesting cases are.

Near the freezing line, temp_c alone tells you almost nothing about whether precipitation is falling as rain or ice pellets. We’ve written about the precip-type problem before, but the same principle generalises: any single field that crosses a boundary cleanly in theory tends to be messiest in practice right at that boundary, because that’s where atmospheric conditions are most variable and where your source data (METAR observations, NWP model output, interpolated readings) is most likely to disagree with actual surface truth.

Compound detection shifts the logic from “did one field cross a threshold” to “do multiple fields together indicate a physical state.” It’s closer to how a meteorologist reads a sounding than how a developer first builds a weather alert.

Building a Compound State: Black Ice Risk as an Example

Black ice is a good example because it’s genuinely hazardous and genuinely invisible to simple threshold checks. The conditions aren’t exotic: a wet surface (recent precip or heavy overnight dew), temperatures near or just below freezing, and light wind to keep surface temperatures from equalising with air temperatures too quickly.

A single-field check on temp_c < 0 misses cases where the surface is below freezing but the air temperature reading hasn’t caught up — surface temperatures can lead air temps by 2–3°C on clear, calm nights, particularly because standard stations measure at 1.5–2m height, not at road level. It also flags plenty of situations that are legitimately cold but dry, where black ice won’t form.

A compound check looks like this:

  • temp_c between -3 and +1 (air temp near freezing, not deep winter cold)
  • feelslike_c noticeably below temp_c (surface radiation likely driving surface below air temp)
  • precip_mm > 0 in the past 1–3 hours, OR humidity > 90%
  • wind_kph < 10 (light wind — turbulent mixing would reduce the surface-to-air temperature gap)
  • Hour of the day between 22:00 and 08:00 local (peak radiative cooling window)

No individual one of those is sufficient. Together they describe a physical scenario with a meaningful probability of ice formation. You can tune the weights — maybe humidity > 90% alone isn’t enough without recent precip, maybe the temp window is tighter in your region. The structure is: identify the physical state, then find the minimal set of API fields that triangulate it.

Implementation Pattern: Scoring Over Thresholds

Hard thresholds are brittle. A scoring model is more stable because it degrades gracefully when some fields are ambiguous.

Assign a weight to each contributing signal, sum the weights that are satisfied, and flag the compound condition when the total exceeds a minimum score. Here’s a minimal Python sketch:

def black_ice_risk_score(obs):
    score = 0

    temp = obs.get('temp_c', 999)
    feels = obs.get('feelslike_c', 999)
    humidity = obs.get('humidity', 0)
    wind = obs.get('wind_kph', 999)
    precip = obs.get('precip_mm', 0)
    local_hour = obs.get('local_hour', 12)

    if -3 <= temp <= 1:
        score += 3
    elif -5 <= temp <= -3 or 1 < temp <= 2:
        score += 1

    if (temp - feels) > 1.5:
        score += 2

    if precip > 0:
        score += 3
    elif humidity > 92:
        score += 1

    if wind < 5:
        score += 2
    elif wind < 10:
        score += 1

    if local_hour >= 22 or local_hour <= 7:
        score += 1

    return score


risk = black_ice_risk_score(current_obs)
if risk >= 7:
    flag = 'high'
elif risk >= 4:
    flag = 'elevated'
else:
    flag = 'low'

You can pull local_hour from the localtime field in the API response — parse it and extract the hour. The feelslike_c, temp_c, humidity, wind_kph, and precip_mm fields are all available in both the realtime and hourly forecast endpoints.

Three Other Compound States Worth Building

Hypothermia exposure risk. Not about absolute cold — about the combination of temperature, wind, wetness, and humidity. A wet hiker at 10°C with 40 kph wind and rain is at more risk than a dry, still-air day at -5°C in the right gear. Fields involved: temp_c, wind_kph, precip_mm, humidity, feelslike_c. The feelslike_c calculation already incorporates wind chill at low temperatures and heat index at high ones, but it doesn’t incorporate wetness — so precip_mm and humidity need to be separate signals.

Dense radiation fog onset. Distinct from fog that’s already present — you’re trying to catch the next 2–3 hours before the condition code flips. Signals: humidity rising across consecutive hourly records, dew point spread (temp_c minus dewpoint_c, or derive it from humidity and temp if your response doesn’t expose it directly), wind under 5 kph, and clear skies (cloud coverage under 10%). Radiation fog needs clear skies to allow overnight surface cooling; cloud cover above roughly 30–40% usually suppresses it. That’s a signal you can use in the negative: if cloud is high, drop the fog-onset score regardless of humidity.

High-risk outdoor construction window closure. Scaffolding and crane operations are typically halted above specific wind speeds — commonly 35–50 kph for cranes depending on the lift — but also when visibility drops or lightning is nearby. A compound flag combining sustained wind_kph, gust_kph, condition codes in the 1087–1276 range (thunderstorm family), and short-range forecast trend gives you something more useful than any single value. The gust matters more than sustained wind for most crane-stop decisions: a 20-kph mean with 48-kph gusts is a stop condition even though the mean looks fine.

Where This Gets Harder

Compound detection assumes your input fields are internally consistent. They’re not always. A METAR-sourced humidity and a model-derived temperature from a different grid cell can conflict in ways that produce physically implausible combinations — dew point spread of 0.1°C alongside a clear-sky condition code, for instance. This isn’t common, but it happens more in regions with sparse station coverage where the API is leaning harder on interpolated model output rather than actual observations.

The practical fix is to build sanity checks into your scoring function before accumulating score: if the combination of inputs is physically incoherent, reduce your confidence in the compound flag rather than trusting the aggregate. Something as simple as “if humidity is below 60%, dew-point-spread signals can’t contribute to fog scoring” catches a lot of the inconsistency cases without much code.

There’s also the forecast question. Compound detection on realtime data is a “right now” assessment. If you’re running it on hourly forecast data for look-ahead, the further out you go, the less correlated adjacent fields become — GFS at hour 72 is coherent within the model, but the uncertainty on any individual field is wide enough that a compound score built from four uncertain fields can give false confidence in either direction. Past roughly 24 hours, compound logic should surface an uncertainty caveat to your users, not a hard flag.

Start by logging the raw input values alongside every compound flag your system fires. After a week or two you’ll see which signals are doing the work and which are adding noise — and you’ll find the thresholds that match your actual geography and use case, not the ones that looked reasonable in a notebook.

Scroll to Top