Why weather_code Alone Won’t Cut It: Building a Severity Scale from Raw API Fields

Condition codes are a convenience, not a measurement. WeatherAPI’s condition.code field gives you something like “heavy rain” or “blizzard” — a human-readable category. What it doesn’t give you is whether that blizzard is 15 cm/hr of snowfall with 70 kph gusts or a borderline event that barely cleared the label. That gap matters the moment you try to build anything that acts on severity rather than just presence.

The number itself carries no intensity information. Code 1225 maps to “blizzard” regardless of whether visibility is 50 meters or 500. Code 1195 maps to “heavy rain” whether you’re looking at 8 mm/hr or 25 mm/hr. If you’re routing a delivery fleet, alerting field workers, or deciding whether to shut down outdoor equipment, the difference between those two ends of the range is exactly what you need to know — and the condition code hides it.

What the Raw Fields Actually Give You

The hourly forecast payload from WeatherAPI contains fields that carry real intensity signal. The ones worth building on:

  • precip_mm — per-hour accumulation, the actual rate
  • wind_kph and gust_kph — sustained vs. peak; the gap between them matters too
  • vis_km — visibility, which degrades sharply in heavy precip, fog, or blowing snow
  • snow_cm — snowfall rate, distinct from rain-equivalent precip
  • pressure_mb and its trend across adjacent hours
  • feelslike_c — combines wind and temperature, relevant for cold-weather operations

None of these alone tells you severity. Weighted together against a defined threshold table, though, they produce something far more actionable than a code.

A Practical Scoring Approach

The simplest useful structure is a 0–10 severity scale computed per hour, where each raw field contributes points based on where its value falls within a threshold range. You pick the thresholds based on what matters for your domain — agriculture, construction, and maritime operations have genuinely different tolerances, so a generic scale is less useful than a domain-parameterized one.

Here’s a minimal Python sketch of the idea:

def severity_score(hour: dict) -> float:
    score = 0.0

    # Rain rate
    p = hour.get("precip_mm", 0)
    if p >= 20:   score += 3.0
    elif p >= 10: score += 2.0
    elif p >= 4:  score += 1.0

    # Snow rate
    s = hour.get("snow_cm", 0)
    if s >= 5:    score += 3.0
    elif s >= 2:  score += 1.5
    elif s >= 0.5: score += 0.5

    # Gusts
    g = hour.get("gust_kph", 0)
    if g >= 90:   score += 2.5
    elif g >= 60: score += 1.5
    elif g >= 40: score += 0.5

    # Visibility
    v = hour.get("vis_km", 10)
    if v <= 0.2:  score += 2.5
    elif v <= 1:  score += 1.5
    elif v <= 4:  score += 0.5

    # Feels-like cold exposure
    f = hour.get("feelslike_c", 15)
    if f <= -20:  score += 2.0
    elif f <= -10: score += 1.0
    elif f <= 0:  score += 0.5

    return min(score, 10.0)

Run that across a 24- or 48-hour forecast window and you get a per-hour severity trace rather than a flat "heavy rain from 14:00 to 18:00" label. That trace is immediately useful for scheduling: severity below 2.0, proceed. Spikes above 5.0 for two or more consecutive hours, pause operations. That's a real decision rule, not a vibes-based flag on a condition code.

Where Condition Codes Still Matter

Condition codes are still worth keeping in the pipeline — they're just the wrong tool for severity. They're the right tool for two things: display labels (users want to see "heavy snow", not "severity 6.4") and type discrimination. A score of 4.5 driven entirely by wind with no precipitation is a very different operational situation from a 4.5 driven by snow rate and poor visibility, even if the number is identical. The condition code helps distinguish those: it tells you which raw fields are likely dominating, even if it doesn't say by how much.

The pattern that works well: compute severity from raw fields, use the condition code to annotate the type of severity. Store both. Surface the severity score to your logic layer and the condition string to your presentation layer.

The Gust-to-Sustained Ratio as a Hidden Signal

One field combination that tends to get ignored: the ratio of gust_kph to wind_kph. A ratio above roughly 1.5 indicates highly gusty, turbulent airflow rather than a steady strong wind. That matters differently for crane operations — where gusts are the actual hazard, not mean wind speed — versus calculating drift on a marine route, where sustained wind matters more. Computing the ratio is three lines of code. Most condition-code-based approaches skip it entirely.

In the scoring function above, you could replace the flat gust threshold with:

gust_ratio = g / max(hour.get("wind_kph", 1), 1)
if gust_ratio > 1.6 and g > 40:
    score += 1.0  # Extra weight for gusty vs. steady wind

Small addition, but it separates "40 kph steady" from "40 kph average with 65 kph gusts" — which are genuinely different hazards.

Pressure Trend as an Early Warning Layer

The other thing a condition code can't do: tell you what's developing in the next two to four hours. A rapid pressure drop across three consecutive hourly readings is a reliable indicator of incoming intensification even before the precip and wind fields respond. You can compute it inline from the forecast array:

def pressure_drop_score(hours: list) -> float:
    if len(hours) < 4:
        return 0.0
    drop = hours[0]["pressure_mb"] - hours[3]["pressure_mb"]
    if drop >= 4:   return 2.0
    elif drop >= 2: return 1.0
    return 0.0

Apply that to the first hour in your evaluation window and add it to that hour's severity score as a leading indicator. One real caveat: pressure trend in a model forecast doesn't carry the same diagnostic weight it does in real-time observed data — NWS and ECMWF both note that rapid cyclogenesis signatures in model output tend to be smoothed relative to what actually verifies. It's still signal worth including, just not the same thing as watching a barometer fall in real time.

One Honest Caveat About Forecast Horizon

The raw fields in a forecast response are model output, not observations. precip_mm at hour 36 is a probabilistic estimate from GFS or HRRR with real uncertainty attached, even if the API returns a single number. A severity score computed from those fields inherits that uncertainty. Forecast skill for precipitation intensity degrades faster than skill for temperature or wind direction — by day 3, hourly precip rates from GFS are more useful as rough magnitude guidance than as precise values to threshold against.

If you're using severity scores to drive automated decisions rather than UI display, building in a confidence weight that scales with forecast horizon is worth doing. Discount the raw field values for hours beyond 24–48; don't just clip the score. The condition code will still say "heavy rain" at hour 60 with the same apparent confidence it has at hour 2. The raw fields, at least, give you something to apply your own uncertainty model to.

If you're currently gating any operational logic on condition.code alone, pull the underlying fields for the same hours and look at the variance within a single code label. The spread between the minimum and maximum values you find within one code is a reasonable proxy for how much information you're leaving on the table — and usually that spread is wider than people expect until they actually look.

Scroll to Top