7 min read

The NFL season stretches from September through February, exposing teams to the full spectrum of weather conditions. From scorching early-season heat in Arizona to frigid January games in Green Bay, weather is an unavoidable factor that affects...

Chapter 24: Weather Effects

Part 5: Advanced Topics


Learning Objectives

By the end of this chapter, you will be able to:

  1. Quantify the impact of weather conditions on NFL game outcomes
  2. Build weather adjustment models for scoring and spread predictions
  3. Understand which weather factors matter most and why
  4. Integrate weather data into prediction pipelines
  5. Identify when weather creates analytical opportunities

Introduction: Football in the Elements

The NFL season stretches from September through February, exposing teams to the full spectrum of weather conditions. From scorching early-season heat in Arizona to frigid January games in Green Bay, weather is an unavoidable factor that affects gameplay. For analysts, weather represents a quantifiable variable that can improve predictions—if properly understood.

This chapter develops a systematic framework for incorporating weather into NFL analysis. We'll examine which conditions matter, how much they affect outcomes, and how to build robust weather adjustment models.


24.1 The Weather Landscape of the NFL

Indoor vs Outdoor Venues

As of 2024, the NFL has a mix of venue types:

Fully Indoor (8 teams): - AT&T Stadium (Cowboys) - Caesars Superdome (Saints) - Mercedes-Benz Stadium (Falcons) - U.S. Bank Stadium (Vikings) - Allegiant Stadium (Raiders) - SoFi Stadium (Rams, Chargers) - State Farm Stadium (Cardinals) - Ford Field (Lions)

Retractable Roof (4 teams): - Lucas Oil Stadium (Colts) - NRG Stadium (Texans) - Hard Rock Stadium (Dolphins, partial)

Outdoor (20 teams): - All others, including cold-weather markets

This distribution means roughly 40% of regular season games are played indoors, with the percentage dropping in playoffs as cold-weather outdoor teams advance.

Seasonal Weather Patterns

NFL weather follows predictable patterns:

Period Typical Conditions Impact Level
Sep-Oct Warm, variable Low
Nov Cooling, some cold Moderate
Dec Cold, precipitation High
Jan (playoffs) Extreme cold possible High
Feb (Super Bowl) Indoor/warm (neutral site) Minimal

24.2 Temperature Effects

The Cold Weather Impact

Temperature affects gameplay through multiple mechanisms:

Physiological Effects: - Muscle stiffness increases - Hand dexterity decreases - Grip strength reduces - Fatigue patterns change

Equipment Effects: - Ball becomes harder and slicker - Grip aids less effective - Cleats interact differently with surface

Tactical Effects: - Passing game generally suffers - Running game relatively favored - Kicking becomes less reliable

Quantifying Temperature Impact

Historical analysis reveals temperature thresholds:

Temperature Zones and Scoring Impact:

> 60°F (Mild): Baseline - no adjustment
50-60°F (Cool): -0.3 total points
40-50°F (Cold): -1.5 total points
30-40°F (Very Cold): -3.0 total points
20-30°F (Frigid): -5.0 total points
< 20°F (Extreme): -7.0+ total points

Regression Analysis:

# Approximate relationship
total_points_adjustment = -0.15 * max(0, 55 - temperature)

# Example: 25°F game
adjustment = -0.15 * (55 - 25) = -4.5 total points

Spread vs Total Effects

Temperature affects totals more than spreads:

  • Totals: Strong relationship (cold reduces scoring)
  • Spreads: Weaker relationship (affects both teams similarly)

However, asymmetric effects exist: - Pass-heavy offenses suffer more in cold - Teams acclimated to cold have advantage

Cold Weather Spread Adjustment:

If Temp < 30°F and home team is cold-weather:
    Home advantage += 0.5 to 1.0 points

If Temp < 30°F and away team is from warm/dome:
    Home advantage += 1.0 to 2.0 points

24.3 Wind Effects

Why Wind Matters Most

Wind is often the most impactful weather factor because:

  1. Directly affects ball flight - Passes and kicks
  2. Asymmetric by possession - Teams take turns facing wind
  3. Harder to prepare for - Can't practice in wind tunnel
  4. Variable during games - Conditions can change

Wind Speed Thresholds

Wind Impact Zones:

0-10 mph: Minimal impact
10-15 mph: Noticeable on deep passes/kicks
15-20 mph: Significant impact on passing game
20-25 mph: Major impact, strategies adjust
> 25 mph: Extreme, fundamentally changes game

Quantifying Wind Effects

On Passing:

def wind_passing_adjustment(wind_mph: float) -> float:
    """
    Adjust passing EPA expectation for wind.

    Returns:
        Multiplier for passing EPA (1.0 = no change)
    """
    if wind_mph <= 10:
        return 1.0
    elif wind_mph <= 15:
        return 0.95 - 0.01 * (wind_mph - 10)
    elif wind_mph <= 20:
        return 0.90 - 0.02 * (wind_mph - 15)
    else:
        return 0.80 - 0.02 * (wind_mph - 20)

On Kicking:

def wind_kick_adjustment(wind_mph: float, into_wind: bool) -> float:
    """
    Adjust field goal probability for wind.

    Returns:
        Multiplier for base FG probability
    """
    base_reduction = 0.02 * max(0, wind_mph - 10)

    if into_wind:
        # Kicks into wind lose distance
        return 1.0 - base_reduction * 1.5
    else:
        # Kicks with wind benefit slightly
        return 1.0 + base_reduction * 0.3

Total Points Adjustment for Wind

Wind Speed → Total Adjustment:

< 10 mph: 0 points
10-15 mph: -1.5 points
15-20 mph: -3.5 points
20-25 mph: -6.0 points
> 25 mph: -9.0+ points

24.4 Precipitation Effects

Types of Precipitation

Rain: - Most common precipitation type - Affects grip and footing - Ball becomes slippery - Passing accuracy decreases

Snow: - Dramatic visual impact - Field markings obscured - Running game advantages - Passing significantly impaired

Freezing Rain/Sleet: - Most dangerous condition - Extreme footing issues - Rarely played through (games postponed)

Quantifying Precipitation Impact

Rain Intensity → Adjustment:

Light rain: -1.0 total points
Moderate rain: -2.5 total points
Heavy rain: -4.0 total points

Snow Intensity → Adjustment:

Light snow: -2.0 total points
Moderate snow: -4.0 total points
Heavy snow: -6.0+ total points

Play Selection in Precipitation

Historical analysis of play calling:

Condition Run % Pass % Pass EPA Change
Clear 42% 58% Baseline
Light rain 45% 55% -8%
Heavy rain 50% 50% -18%
Snow 52% 48% -22%

Teams adjust play calling, but not always optimally. Value may exist in identifying when adjustments are insufficient.


24.5 Combined Weather Effects

The Weather Index

Rather than treating conditions separately, create a combined metric:

def calculate_weather_index(temperature: float, wind: float,
                           precipitation: str, humidity: float) -> float:
    """
    Calculate combined weather severity index.

    Args:
        temperature: Temperature in Fahrenheit
        wind: Wind speed in mph
        precipitation: 'none', 'light_rain', 'heavy_rain', 'snow'
        humidity: Relative humidity percentage

    Returns:
        Weather index (0 = perfect, higher = worse conditions)
    """
    # Temperature component
    temp_factor = max(0, 55 - temperature) * 0.1

    # Wind component
    wind_factor = max(0, wind - 10) * 0.15

    # Precipitation component
    precip_factors = {
        'none': 0,
        'light_rain': 1.5,
        'heavy_rain': 3.5,
        'light_snow': 2.5,
        'heavy_snow': 5.0
    }
    precip_factor = precip_factors.get(precipitation, 0)

    # Humidity (affects heat, minimal in cold)
    heat_index = max(0, temperature - 85) * humidity / 100 * 0.05

    return temp_factor + wind_factor + precip_factor + heat_index

Weather Index Interpretation

Weather Index Conditions Total Adjustment
0-1 Ideal 0 points
1-3 Slightly adverse -1 to -2 points
3-5 Moderate impact -3 to -5 points
5-8 Significant impact -5 to -8 points
> 8 Extreme -8+ points

24.6 Altitude Effects

The Denver Factor

Mile High Stadium sits at 5,280 feet elevation, creating unique challenges:

Physiological Effects: - Reduced oxygen availability - Faster fatigue for visiting teams - Takes 24-48 hours to acclimate

Ball Flight Effects: - Less air resistance - Passes and kicks travel further - Punts harder to control

Quantifying Altitude

Denver-Specific Adjustments:

Field Goals: +2-3 yards of effective range
Total Points: +1.5 points (easier scoring)
Home Advantage: +0.5 to +1.0 points additional

Visiting Team Adjustment:
- Sea level team: -0.5 to -1.0 points
- Team that flew in game day: -1.0 to -1.5 points

Other Elevation Venues

Stadium Elevation Impact
Empower Field (DEN) 5,280 ft Significant
State Farm (ARI) 1,071 ft Minimal
Most others < 1,000 ft None

Only Denver requires meaningful altitude adjustment.


24.7 Heat and Humidity

Early Season Heat

September and early October can bring extreme heat:

Heat Thresholds:

< 85°F: No significant impact
85-90°F: Slight fatigue increase
90-95°F: Noticeable player fatigue
> 95°F: Significant heat impact

Heat + Humidity: Heat index (feels-like temperature) matters more than actual temperature:

def heat_index_adjustment(temperature: float, humidity: float) -> float:
    """
    Calculate scoring adjustment for heat.

    Returns:
        Points adjustment (usually negative for extreme heat)
    """
    if temperature < 85:
        return 0

    # Simple heat index approximation
    heat_index = temperature + 0.5 * (humidity - 50)

    if heat_index > 100:
        return -2.0 - 0.1 * (heat_index - 100)
    elif heat_index > 90:
        return -0.5 - 0.15 * (heat_index - 90)
    else:
        return 0

Home Team Heat Advantage

Teams acclimated to heat have advantages:

Heat Advantage (when temp > 85°F):

Warm-weather home team vs cold-weather visitor:
    +0.5 to +1.5 additional home advantage

Indoor team visiting outdoor hot game:
    -0.5 to -1.0 points

24.8 Building a Weather Model

Data Requirements

To build a weather model, collect:

  1. Historical weather data (temperature, wind, precipitation)
  2. Game outcomes (scores, margins)
  3. Team characteristics (indoor/outdoor, climate zone)
  4. Venue information (roof status, elevation)

Model Architecture

class NFLWeatherModel:
    """Complete weather adjustment model for NFL predictions."""

    def __init__(self):
        self.baseline_total = 45.0
        self.baseline_std = 13.5

    def adjust_total(self, forecast: Dict) -> float:
        """
        Adjust total points prediction for weather.

        Args:
            forecast: Dict with temperature, wind, precipitation

        Returns:
            Adjusted total points
        """
        temp = forecast.get('temperature', 65)
        wind = forecast.get('wind_mph', 5)
        precip = forecast.get('precipitation', 'none')

        # Temperature adjustment
        temp_adj = -0.12 * max(0, 55 - temp)

        # Wind adjustment
        wind_adj = -0.25 * max(0, wind - 10)

        # Precipitation adjustment
        precip_adjustments = {
            'none': 0, 'light_rain': -1.5, 'heavy_rain': -4.0,
            'light_snow': -2.5, 'heavy_snow': -5.5
        }
        precip_adj = precip_adjustments.get(precip, 0)

        total_adjustment = temp_adj + wind_adj + precip_adj

        return self.baseline_total + total_adjustment

    def adjust_spread(self, spread: float, home_team: str, away_team: str,
                     forecast: Dict) -> float:
        """
        Adjust spread prediction for weather asymmetries.

        Args:
            spread: Pre-weather spread (home perspective)
            home_team: Home team identifier
            away_team: Away team identifier
            forecast: Weather forecast

        Returns:
            Weather-adjusted spread
        """
        temp = forecast.get('temperature', 65)
        wind = forecast.get('wind_mph', 5)

        # Check for cold-weather advantage
        home_cold_team = self.is_cold_weather_team(home_team)
        away_cold_team = self.is_cold_weather_team(away_team)

        adjustment = 0

        if temp < 30:
            if home_cold_team and not away_cold_team:
                adjustment -= 1.5  # Home advantage increases
            elif not home_cold_team and away_cold_team:
                adjustment += 1.0  # Home advantage decreases

        # Passing team disadvantage in high wind
        if wind > 20:
            home_pass_heavy = self.is_pass_heavy_team(home_team)
            away_pass_heavy = self.is_pass_heavy_team(away_team)

            if home_pass_heavy and not away_pass_heavy:
                adjustment += 1.0  # Home team hurt more
            elif away_pass_heavy and not home_pass_heavy:
                adjustment -= 1.0  # Away team hurt more

        return spread + adjustment

    def is_cold_weather_team(self, team: str) -> bool:
        """Check if team plays in cold-weather outdoor stadium."""
        cold_teams = ['GB', 'CHI', 'NE', 'BUF', 'PIT', 'CLE', 'CIN',
                     'DEN', 'KC', 'PHI', 'NYG', 'NYJ', 'BAL', 'WAS']
        return team in cold_teams

    def is_pass_heavy_team(self, team: str) -> bool:
        """Check if team relies heavily on passing."""
        # Would be dynamic based on season data
        return False  # Placeholder

Model Validation

Validate weather model by:

  1. Backtesting: Apply to historical games
  2. Comparing to market: Are markets already pricing weather?
  3. Out-of-sample testing: Reserve recent seasons
  4. Calibration check: Are adjustments properly sized?
def validate_weather_model(model, games_df):
    """
    Validate weather model against historical data.

    Args:
        model: Weather adjustment model
        games_df: DataFrame with weather and outcomes

    Returns:
        Validation metrics
    """
    results = {
        'total_mae_baseline': 0,
        'total_mae_adjusted': 0,
        'spread_mae_baseline': 0,
        'spread_mae_adjusted': 0
    }

    for _, game in games_df.iterrows():
        forecast = {
            'temperature': game['temperature'],
            'wind_mph': game['wind_speed'],
            'precipitation': game['precipitation']
        }

        # Total adjustment
        adjusted_total = model.adjust_total(forecast)
        actual_total = game['home_score'] + game['away_score']

        baseline_error = abs(45 - actual_total)
        adjusted_error = abs(adjusted_total - actual_total)

        results['total_mae_baseline'] += baseline_error
        results['total_mae_adjusted'] += adjusted_error

    n = len(games_df)
    results['total_mae_baseline'] /= n
    results['total_mae_adjusted'] /= n

    results['improvement'] = (results['total_mae_baseline'] -
                             results['total_mae_adjusted'])

    return results

24.9 Weather Data Sources

Primary Sources

National Weather Service (NWS) - Free, official forecasts - API available - Historical data accessible - https://www.weather.gov/

OpenWeather API - Free tier available - Forecast and historical - Easy integration - https://openweathermap.org/api

Visual Crossing - Historical weather data - Stadium-specific locations - Reasonable pricing - https://www.visualcrossing.com/

Weather Data Best Practices

  1. Get game-time forecasts - Not daily averages
  2. Use stadium location - Not city center
  3. Check roof status - Retractable roofs may close
  4. Update before game - Weather changes
  5. Store forecasts - For validation

Example: Fetching Weather Data

import requests
from datetime import datetime

def get_game_weather(lat: float, lon: float, game_datetime: datetime,
                    api_key: str) -> Dict:
    """
    Fetch weather forecast for game location and time.

    Args:
        lat: Stadium latitude
        lon: Stadium longitude
        game_datetime: Game start time
        api_key: OpenWeather API key

    Returns:
        Weather conditions dict
    """
    # Historical data (for past games)
    if game_datetime < datetime.now():
        url = f"https://api.openweathermap.org/data/3.0/onecall/timemachine"
        params = {
            'lat': lat,
            'lon': lon,
            'dt': int(game_datetime.timestamp()),
            'appid': api_key,
            'units': 'imperial'
        }
    else:
        # Forecast
        url = f"https://api.openweathermap.org/data/2.5/forecast"
        params = {
            'lat': lat,
            'lon': lon,
            'appid': api_key,
            'units': 'imperial'
        }

    response = requests.get(url, params=params)
    data = response.json()

    # Parse relevant fields
    return {
        'temperature': data['main']['temp'],
        'wind_mph': data['wind']['speed'],
        'humidity': data['main']['humidity'],
        'precipitation': parse_precipitation(data),
        'conditions': data['weather'][0]['description']
    }

24.10 Market Pricing of Weather

Are Markets Efficient?

Markets generally price weather well:

Well-Priced: - Extreme cold in December/January - High-profile weather games (snow bowls) - Obvious conditions (forecast rain)

Potentially Mispriced: - Wind (often underweighted) - Heat/humidity (early season) - Last-minute weather changes

Finding Weather Value

def identify_weather_opportunity(model_adjustment: float,
                                 line_movement: float,
                                 weather_type: str) -> Dict:
    """
    Identify if weather creates betting value.

    Args:
        model_adjustment: Your weather adjustment
        line_movement: How much line moved after weather known
        weather_type: Type of weather condition

    Returns:
        Opportunity assessment
    """
    # Compare model to market
    market_adjustment = line_movement
    difference = model_adjustment - market_adjustment

    # Determine if opportunity exists
    threshold = 1.0 if weather_type == 'wind' else 0.5

    return {
        'model_adjustment': model_adjustment,
        'market_adjustment': market_adjustment,
        'difference': difference,
        'potential_value': abs(difference) > threshold,
        'direction': 'under' if difference < 0 else 'over'
    }

24.11 Case Study: Historic Weather Games

The "Ice Bowl" - 1967 NFL Championship

Conditions: - Temperature: -13°F - Wind chill: -36°F - Green Bay vs Dallas

Impact: - Passing severely limited - Field frozen solid - One of coldest games ever

Analysis: Our model would suggest -10 to -12 points total adjustment. Actual combined score: 34 points (21-17 Packers) Era-adjusted scoring was lower than modern games.

Snow Games Analysis

Historic snow games show consistent patterns:

Game Snow Intensity Combined Score vs Expected
Lions-Eagles 2013 Heavy 65 +15
Colts-Bills 2017 Heavy 24 -25
Bills-Browns 2007 Moderate 18 -30

Key Finding: Snow game variance is extremely high. Model adjustments are averages, but individual outcomes swing wildly.


24.12 Integrating Weather into Predictions

The Complete Workflow

class NFLPredictionWithWeather:
    """Prediction system incorporating weather."""

    def __init__(self, base_model, weather_model):
        self.base_model = base_model
        self.weather_model = weather_model

    def predict(self, home_team: str, away_team: str,
               venue: str, game_time: datetime) -> Dict:
        """
        Generate prediction incorporating weather.

        Args:
            home_team: Home team identifier
            away_team: Away team identifier
            venue: Stadium identifier
            game_time: Game start time

        Returns:
            Complete prediction with weather adjustments
        """
        # Get base prediction (no weather)
        base = self.base_model.predict(home_team, away_team)

        # Get weather forecast
        weather = self.get_weather_forecast(venue, game_time)

        # Check if indoor
        if self.is_indoor_game(venue, weather):
            return {
                'spread': base['spread'],
                'total': base['total'],
                'weather_adjusted': False,
                'weather': 'Indoor'
            }

        # Apply weather adjustments
        adjusted_total = self.weather_model.adjust_total(weather)
        adjusted_spread = self.weather_model.adjust_spread(
            base['spread'], home_team, away_team, weather
        )

        # Calculate adjustment sizes
        total_adj = adjusted_total - base['total']
        spread_adj = adjusted_spread - base['spread']

        return {
            'spread': adjusted_spread,
            'total': adjusted_total,
            'base_spread': base['spread'],
            'base_total': base['total'],
            'spread_adjustment': spread_adj,
            'total_adjustment': total_adj,
            'weather': weather,
            'weather_adjusted': True
        }

    def is_indoor_game(self, venue: str, weather: Dict) -> bool:
        """Determine if game is effectively indoors."""
        indoor_venues = ['ATT', 'DOME', 'MERC', 'USB', 'ALLE', 'SOFI',
                        'STAT', 'FORD']
        if venue in indoor_venues:
            return True

        # Check retractable roof status
        retractable = ['LUCA', 'NRG', 'HARD']
        if venue in retractable:
            # Roof typically closed if rain/cold
            if weather.get('precipitation') != 'none':
                return True
            if weather.get('temperature', 70) < 50:
                return True

        return False

Summary

Weather effects on NFL games can be quantified:

  1. Temperature: Cold reduces scoring, with -0.12 points per degree below 55°F
  2. Wind: Most impactful factor, especially >15 mph
  3. Precipitation: Rain and snow reduce totals by 1.5-5.5 points
  4. Altitude: Only Denver has meaningful impact
  5. Heat: Early season extreme heat causes modest effects

Key principles: - Weather affects totals more than spreads - Asymmetric team effects create spread opportunities - Markets generally price weather efficiently - Wind is often underweighted by markets - Variance increases significantly in bad weather


Key Formulas

Temperature Adjustment:

Total_Adj = -0.12 × max(0, 55 - Temperature)

Wind Adjustment:

Total_Adj = -0.25 × max(0, Wind_mph - 10)

Combined Weather Index:

Index = Temp_Factor + Wind_Factor + Precip_Factor
Where each factor is weighted by impact

Practice Problems

  1. Calculate the total adjustment for a game with 28°F temperature and 18 mph winds.

  2. A dome team visits Green Bay in December (15°F, 12 mph wind). Estimate the additional home field advantage.

  3. Design a system to automatically fetch and integrate weather data into weekly predictions.


Chapter 24 Summary

Weather is a quantifiable factor that meaningfully affects NFL game outcomes. By systematically measuring temperature, wind, precipitation, and their interactions, analysts can improve predictions. While markets efficiently price many weather effects, opportunities exist—particularly around wind and last-minute forecast changes.


Looking Ahead

Chapter 25 examines Home Field Advantage Deep Dive—moving beyond the standard 2.5-3 point home advantage to understand what drives it, how it varies by team and situation, and how it has evolved over time.