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...
In This Chapter
- Part 5: Advanced Topics
- Learning Objectives
- Introduction: Football in the Elements
- 24.1 The Weather Landscape of the NFL
- 24.2 Temperature Effects
- 24.3 Wind Effects
- 24.4 Precipitation Effects
- 24.5 Combined Weather Effects
- 24.6 Altitude Effects
- 24.7 Heat and Humidity
- 24.8 Building a Weather Model
- 24.9 Weather Data Sources
- 24.10 Market Pricing of Weather
- 24.11 Case Study: Historic Weather Games
- 24.12 Integrating Weather into Predictions
- Summary
- Key Formulas
- Practice Problems
- Chapter 24 Summary
- Looking Ahead
Chapter 24: Weather Effects
Part 5: Advanced Topics
Learning Objectives
By the end of this chapter, you will be able to:
- Quantify the impact of weather conditions on NFL game outcomes
- Build weather adjustment models for scoring and spread predictions
- Understand which weather factors matter most and why
- Integrate weather data into prediction pipelines
- 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:
- Directly affects ball flight - Passes and kicks
- Asymmetric by possession - Teams take turns facing wind
- Harder to prepare for - Can't practice in wind tunnel
- 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:
- Historical weather data (temperature, wind, precipitation)
- Game outcomes (scores, margins)
- Team characteristics (indoor/outdoor, climate zone)
- 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:
- Backtesting: Apply to historical games
- Comparing to market: Are markets already pricing weather?
- Out-of-sample testing: Reserve recent seasons
- 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
- Get game-time forecasts - Not daily averages
- Use stadium location - Not city center
- Check roof status - Retractable roofs may close
- Update before game - Weather changes
- 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:
- Temperature: Cold reduces scoring, with -0.12 points per degree below 55°F
- Wind: Most impactful factor, especially >15 mph
- Precipitation: Rain and snow reduce totals by 1.5-5.5 points
- Altitude: Only Denver has meaningful impact
- 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
-
Calculate the total adjustment for a game with 28°F temperature and 18 mph winds.
-
A dome team visits Green Bay in December (15°F, 12 mph wind). Estimate the additional home field advantage.
-
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.