APM

>Agent Skill

@datadrivenconstruction/weather-impact-analysis

skilldevelopment

Analyze weather data impact on construction schedules. Predict weather delays, optimize work scheduling based on forecasts, and calculate weather-related risk factors for project planning.

apm::install
$apm install @datadrivenconstruction/weather-impact-analysis
apm::skill.md
---
name: "weather-impact-analysis"
description: "Analyze weather data impact on construction schedules. Predict weather delays, optimize work scheduling based on forecasts, and calculate weather-related risk factors for project planning."
homepage: "https://datadrivenconstruction.io"
metadata: {"openclaw": {"emoji": "🚀", "os": ["darwin", "linux", "win32"], "homepage": "https://datadrivenconstruction.io", "requires": {"bins": ["python3"]}}}
---
# Weather Impact Analysis

## Overview

This skill implements weather data analysis for construction project management. Integrate weather forecasts, historical data, and activity sensitivity to predict delays and optimize scheduling.

**Capabilities:**
- Weather forecast integration
- Activity weather sensitivity mapping
- Delay prediction and quantification
- Schedule optimization based on weather
- Historical weather impact analysis
- Risk factor calculation

## Quick Start

```python
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from typing import List, Dict, Optional
from enum import Enum
import requests

class WeatherCondition(Enum):
    CLEAR = "clear"
    CLOUDY = "cloudy"
    RAIN = "rain"
    HEAVY_RAIN = "heavy_rain"
    SNOW = "snow"
    FROST = "frost"
    HIGH_WIND = "high_wind"
    EXTREME_HEAT = "extreme_heat"
    EXTREME_COLD = "extreme_cold"

@dataclass
class WeatherDay:
    date: date
    condition: WeatherCondition
    temp_high: float
    temp_low: float
    precipitation_mm: float
    wind_speed_kmh: float
    humidity_pct: float

@dataclass
class ActivitySensitivity:
    activity_type: str
    min_temp: float
    max_temp: float
    max_wind: float
    max_precipitation: float
    can_work_in_rain: bool

def check_work_day(weather: WeatherDay, activity: ActivitySensitivity) -> Dict:
    """Check if work is possible for given weather and activity"""
    can_work = True
    reasons = []

    if weather.temp_low < activity.min_temp:
        can_work = False
        reasons.append(f"Temperature too low: {weather.temp_low}°C < {activity.min_temp}°C")

    if weather.temp_high > activity.max_temp:
        can_work = False
        reasons.append(f"Temperature too high: {weather.temp_high}°C > {activity.max_temp}°C")

    if weather.wind_speed_kmh > activity.max_wind:
        can_work = False
        reasons.append(f"Wind too strong: {weather.wind_speed_kmh} km/h > {activity.max_wind} km/h")

    if weather.precipitation_mm > activity.max_precipitation and not activity.can_work_in_rain:
        can_work = False
        reasons.append(f"Precipitation: {weather.precipitation_mm}mm")

    return {
        'date': weather.date,
        'can_work': can_work,
        'reasons': reasons,
        'productivity_factor': 1.0 if can_work else 0.0
    }

# Example
concrete_work = ActivitySensitivity(
    activity_type="concrete_placement",
    min_temp=5,
    max_temp=35,
    max_wind=40,
    max_precipitation=2,
    can_work_in_rain=False
)

today_weather = WeatherDay(
    date=date.today(),
    condition=WeatherCondition.RAIN,
    temp_high=15,
    temp_low=8,
    precipitation_mm=10,
    wind_speed_kmh=20,
    humidity_pct=80
)

result = check_work_day(today_weather, concrete_work)
print(f"Can work: {result['can_work']}, Reasons: {result['reasons']}")
```

## Comprehensive Weather Analysis System

### Weather Data Integration

```python
from dataclasses import dataclass, field
from datetime import date, datetime, timedelta
from typing import List, Dict, Optional, Tuple
from enum import Enum
import requests
import json

class WeatherSeverity(Enum):
    NORMAL = 1
    CAUTION = 2
    WARNING = 3
    SEVERE = 4
    EXTREME = 5

@dataclass
class HourlyWeather:
    datetime: datetime
    temperature: float
    feels_like: float
    humidity: float
    wind_speed: float
    wind_direction: float
    precipitation: float
    precipitation_probability: float
    condition: WeatherCondition
    visibility: float
    uv_index: float

@dataclass
class DailyForecast:
    date: date
    temp_high: float
    temp_low: float
    sunrise: datetime
    sunset: datetime
    precipitation_total: float
    precipitation_probability: float
    primary_condition: WeatherCondition
    hourly: List[HourlyWeather] = field(default_factory=list)
    severity: WeatherSeverity = WeatherSeverity.NORMAL

class WeatherDataService:
    """Weather data integration service"""

    def __init__(self, api_key: str = None, provider: str = "openweathermap"):
        self.api_key = api_key
        self.provider = provider
        self.cache: Dict[str, Dict] = {}
        self.cache_duration = timedelta(hours=1)

    def get_forecast(self, latitude: float, longitude: float,
                    days: int = 14) -> List[DailyForecast]:
        """Get weather forecast for location"""
        cache_key = f"{latitude},{longitude}"

        if cache_key in self.cache:
            cached = self.cache[cache_key]
            if datetime.now() - cached['timestamp'] < self.cache_duration:
                return cached['data']

        if self.provider == "openweathermap":
            forecast = self._fetch_openweathermap(latitude, longitude, days)
        else:
            forecast = self._generate_sample_forecast(days)

        self.cache[cache_key] = {
            'timestamp': datetime.now(),
            'data': forecast
        }

        return forecast

    def _fetch_openweathermap(self, lat: float, lon: float,
                              days: int) -> List[DailyForecast]:
        """Fetch from OpenWeatherMap API"""
        url = f"https://api.openweathermap.org/data/2.5/forecast"
        params = {
            'lat': lat,
            'lon': lon,
            'appid': self.api_key,
            'units': 'metric'
        }

        try:
            response = requests.get(url, params=params)
            data = response.json()
            return self._parse_openweathermap(data)
        except Exception as e:
            print(f"Weather API error: {e}")
            return self._generate_sample_forecast(days)

    def _parse_openweathermap(self, data: Dict) -> List[DailyForecast]:
        """Parse OpenWeatherMap response"""
        forecasts = []
        daily_data = {}

        for item in data.get('list', []):
            dt = datetime.fromtimestamp(item['dt'])
            day = dt.date()

            if day not in daily_data:
                daily_data[day] = {
                    'temps': [],
                    'precipitation': 0,
                    'conditions': [],
                    'hourly': []
                }

            daily_data[day]['temps'].append(item['main']['temp'])
            daily_data[day]['precipitation'] += item.get('rain', {}).get('3h', 0)

            condition = self._map_condition(item['weather'][0]['main'])
            daily_data[day]['conditions'].append(condition)

            daily_data[day]['hourly'].append(HourlyWeather(
                datetime=dt,
                temperature=item['main']['temp'],
                feels_like=item['main']['feels_like'],
                humidity=item['main']['humidity'],
                wind_speed=item['wind']['speed'] * 3.6,  # m/s to km/h
                wind_direction=item['wind'].get('deg', 0),
                precipitation=item.get('rain', {}).get('3h', 0),
                precipitation_probability=item.get('pop', 0) * 100,
                condition=condition,
                visibility=item.get('visibility', 10000) / 1000,
                uv_index=0
            ))

        for day, data in daily_data.items():
            primary_condition = max(set(data['conditions']), key=data['conditions'].count)

            forecasts.append(DailyForecast(
                date=day,
                temp_high=max(data['temps']),
                temp_low=min(data['temps']),
                sunrise=datetime.combine(day, datetime.min.time().replace(hour=6)),
                sunset=datetime.combine(day, datetime.min.time().replace(hour=18)),
                precipitation_total=data['precipitation'],
                precipitation_probability=max(h.precipitation_probability for h in data['hourly']),
                primary_condition=primary_condition,
                hourly=data['hourly'],
                severity=self._calculate_severity(primary_condition, data)
            ))

        return sorted(forecasts, key=lambda x: x.date)

    def _map_condition(self, condition_str: str) -> WeatherCondition:
        """Map API condition to enum"""
        mapping = {
            'Clear': WeatherCondition.CLEAR,
            'Clouds': WeatherCondition.CLOUDY,
            'Rain': WeatherCondition.RAIN,
            'Drizzle': WeatherCondition.RAIN,
            'Thunderstorm': WeatherCondition.HEAVY_RAIN,
            'Snow': WeatherCondition.SNOW,
            'Mist': WeatherCondition.CLOUDY,
            'Fog': WeatherCondition.CLOUDY
        }
        return mapping.get(condition_str, WeatherCondition.CLEAR)

    def _calculate_severity(self, condition: WeatherCondition,
                           data: Dict) -> WeatherSeverity:
        """Calculate weather severity"""
        max_temp = max(data['temps'])
        min_temp = min(data['temps'])
        precip = data['precipitation']

        if condition in [WeatherCondition.HEAVY_RAIN, WeatherCondition.SNOW]:
            if precip > 50:
                return WeatherSeverity.EXTREME
            elif precip > 25:
                return WeatherSeverity.SEVERE

        if max_temp > 40 or min_temp < -15:
            return WeatherSeverity.SEVERE

        if max_temp > 35 or min_temp < -5:
            return WeatherSeverity.WARNING

        if condition == WeatherCondition.RAIN:
            return WeatherSeverity.CAUTION

        return WeatherSeverity.NORMAL

    def _generate_sample_forecast(self, days: int) -> List[DailyForecast]:
        """Generate sample forecast for testing"""
        import random
        forecasts = []

        for i in range(days):
            day = date.today() + timedelta(days=i)
            temp_base = 15 + random.uniform(-5, 10)
            condition = random.choice(list(WeatherCondition))

            forecasts.append(DailyForecast(
                date=day,
                temp_high=temp_base + random.uniform(3, 8),
                temp_low=temp_base - random.uniform(3, 8),
                sunrise=datetime.combine(day, datetime.min.time().replace(hour=6)),
                sunset=datetime.combine(day, datetime.min.time().replace(hour=18)),
                precipitation_total=random.uniform(0, 20) if condition == WeatherCondition.RAIN else 0,
                precipitation_probability=random.uniform(0, 100) if condition == WeatherCondition.RAIN else 10,
                primary_condition=condition,
                severity=WeatherSeverity.NORMAL
            ))

        return forecasts
```

### Activity Weather Sensitivity

```python
@dataclass
class WeatherThresholds:
    min_temp: float = -10
    max_temp: float = 45
    max_wind: float = 50
    max_precipitation: float = 50
    max_snow_depth: float = 20
    min_visibility: float = 0.5  # km

@dataclass
class ConstructionActivity:
    activity_id: str
    activity_name: str
    category: str
    thresholds: WeatherThresholds
    indoor: bool = False
    rain_sensitive: bool = True
    frost_sensitive: bool = False
    productivity_factors: Dict[WeatherCondition, float] = field(default_factory=dict)

    def __post_init__(self):
        if not self.productivity_factors:
            self.productivity_factors = {
                WeatherCondition.CLEAR: 1.0,
                WeatherCondition.CLOUDY: 0.95,
                WeatherCondition.RAIN: 0.3 if self.rain_sensitive else 0.8,
                WeatherCondition.HEAVY_RAIN: 0.0 if self.rain_sensitive else 0.5,
                WeatherCondition.SNOW: 0.2,
                WeatherCondition.FROST: 0.5 if self.frost_sensitive else 0.8,
                WeatherCondition.HIGH_WIND: 0.3,
                WeatherCondition.EXTREME_HEAT: 0.6,
                WeatherCondition.EXTREME_COLD: 0.4
            }

class ActivityWeatherAnalyzer:
    """Analyze weather impact on construction activities"""

    # Default activity definitions
    ACTIVITY_TEMPLATES = {
        'concrete_placement': ConstructionActivity(
            activity_id='ACT-001',
            activity_name='Concrete Placement',
            category='structural',
            thresholds=WeatherThresholds(min_temp=5, max_temp=35, max_precipitation=2, max_wind=40),
            rain_sensitive=True,
            frost_sensitive=True
        ),
        'steel_erection': ConstructionActivity(
            activity_id='ACT-002',
            activity_name='Steel Erection',
            category='structural',
            thresholds=WeatherThresholds(max_wind=35, max_precipitation=10),
            rain_sensitive=False
        ),
        'roofing': ConstructionActivity(
            activity_id='ACT-003',
            activity_name='Roofing',
            category='envelope',
            thresholds=WeatherThresholds(min_temp=0, max_precipitation=0, max_wind=30),
            rain_sensitive=True
        ),
        'excavation': ConstructionActivity(
            activity_id='ACT-004',
            activity_name='Excavation',
            category='earthwork',
            thresholds=WeatherThresholds(min_temp=-5, max_precipitation=25),
            rain_sensitive=True,
            frost_sensitive=True
        ),
        'painting_exterior': ConstructionActivity(
            activity_id='ACT-005',
            activity_name='Exterior Painting',
            category='finishing',
            thresholds=WeatherThresholds(min_temp=10, max_temp=35, max_precipitation=0, max_wind=25),
            rain_sensitive=True
        ),
        'masonry': ConstructionActivity(
            activity_id='ACT-006',
            activity_name='Masonry Work',
            category='structural',
            thresholds=WeatherThresholds(min_temp=5, max_temp=32, max_precipitation=5),
            rain_sensitive=True,
            frost_sensitive=True
        ),
        'crane_operations': ConstructionActivity(
            activity_id='ACT-007',
            activity_name='Crane Operations',
            category='equipment',
            thresholds=WeatherThresholds(max_wind=30, min_visibility=1.0),
            rain_sensitive=False
        ),
        'electrical_exterior': ConstructionActivity(
            activity_id='ACT-008',
            activity_name='Exterior Electrical',
            category='MEP',
            thresholds=WeatherThresholds(max_precipitation=0),
            rain_sensitive=True
        ),
        'interior_work': ConstructionActivity(
            activity_id='ACT-009',
            activity_name='Interior Work',
            category='finishing',
            thresholds=WeatherThresholds(),
            indoor=True,
            rain_sensitive=False
        )
    }

    def __init__(self):
        self.activities = dict(self.ACTIVITY_TEMPLATES)

    def add_activity(self, activity: ConstructionActivity):
        """Add custom activity"""
        self.activities[activity.activity_id] = activity

    def analyze_day(self, weather: DailyForecast,
                   activities: List[str]) -> Dict[str, Dict]:
        """Analyze weather impact for specific day and activities"""
        results = {}

        for activity_id in activities:
            activity = self.activities.get(activity_id)
            if not activity:
                continue

            impact = self._calculate_impact(weather, activity)
            results[activity_id] = impact

        return results

    def _calculate_impact(self, weather: DailyForecast,
                         activity: ConstructionActivity) -> Dict:
        """Calculate weather impact on activity"""
        if activity.indoor:
            return {
                'can_work': True,
                'productivity': 1.0,
                'issues': [],
                'recommendations': []
            }

        issues = []
        productivity = 1.0

        # Temperature check
        if weather.temp_low < activity.thresholds.min_temp:
            issues.append(f"Low temperature: {weather.temp_low}°C")
            if activity.frost_sensitive:
                productivity *= 0.0
            else:
                productivity *= 0.5

        if weather.temp_high > activity.thresholds.max_temp:
            issues.append(f"High temperature: {weather.temp_high}°C")
            productivity *= 0.6

        # Precipitation check
        if weather.precipitation_total > activity.thresholds.max_precipitation:
            issues.append(f"Precipitation: {weather.precipitation_total}mm")
            if activity.rain_sensitive:
                productivity *= 0.0
            else:
                productivity *= 0.7

        # Condition-based productivity
        condition_factor = activity.productivity_factors.get(
            weather.primary_condition, 1.0
        )
        productivity *= condition_factor

        # Generate recommendations
        recommendations = []
        if productivity < 0.5 and productivity > 0:
            recommendations.append("Consider rescheduling to more favorable day")
        if weather.temp_low < activity.thresholds.min_temp + 5:
            recommendations.append("Plan for cold weather precautions")
        if weather.precipitation_probability > 50:
            recommendations.append("Have rain contingency plan ready")

        can_work = productivity > 0

        return {
            'activity_name': activity.activity_name,
            'can_work': can_work,
            'productivity': round(productivity, 2),
            'issues': issues,
            'recommendations': recommendations,
            'weather_condition': weather.primary_condition.value,
            'temperature_range': f"{weather.temp_low}°C - {weather.temp_high}°C"
        }

    def find_optimal_days(self, forecast: List[DailyForecast],
                         activity_id: str,
                         min_productivity: float = 0.8) -> List[date]:
        """Find optimal days for an activity"""
        activity = self.activities.get(activity_id)
        if not activity:
            return []

        optimal = []
        for day in forecast:
            impact = self._calculate_impact(day, activity)
            if impact['productivity'] >= min_productivity:
                optimal.append(day.date)

        return optimal
```

### Schedule Weather Integration

```python
from datetime import date, timedelta
from typing import List, Dict
import pandas as pd

@dataclass
class ScheduledActivity:
    activity_id: str
    activity_name: str
    activity_type: str  # Maps to ACTIVITY_TEMPLATES
    planned_start: date
    planned_end: date
    duration_days: int
    is_critical: bool = False

class ScheduleWeatherOptimizer:
    """Optimize construction schedule based on weather"""

    def __init__(self, weather_service: WeatherDataService,
                 activity_analyzer: ActivityWeatherAnalyzer):
        self.weather = weather_service
        self.analyzer = activity_analyzer

    def analyze_schedule(self, schedule: List[ScheduledActivity],
                        location: Tuple[float, float]) -> Dict:
        """Analyze schedule against weather forecast"""
        forecast = self.weather.get_forecast(location[0], location[1])
        forecast_dict = {f.date: f for f in forecast}

        analysis = {
            'activities': [],
            'weather_delays': 0,
            'risk_days': [],
            'recommendations': []
        }

        for activity in schedule:
            activity_analysis = self._analyze_activity(
                activity, forecast_dict
            )
            analysis['activities'].append(activity_analysis)

            if activity_analysis['expected_delay'] > 0:
                analysis['weather_delays'] += activity_analysis['expected_delay']

            analysis['risk_days'].extend(activity_analysis['risk_days'])

        # Generate overall recommendations
        if analysis['weather_delays'] > 5:
            analysis['recommendations'].append(
                f"Schedule shows {analysis['weather_delays']} potential weather delay days. "
                "Consider buffer time or alternative scheduling."
            )

        return analysis

    def _analyze_activity(self, activity: ScheduledActivity,
                         forecast: Dict[date, DailyForecast]) -> Dict:
        """Analyze single activity against weather"""
        result = {
            'activity_id': activity.activity_id,
            'activity_name': activity.activity_name,
            'planned_start': activity.planned_start,
            'planned_end': activity.planned_end,
            'day_analysis': [],
            'risk_days': [],
            'expected_delay': 0,
            'avg_productivity': 1.0
        }

        current_date = activity.planned_start
        productivities = []
        delay_days = 0

        while current_date <= activity.planned_end:
            if current_date in forecast:
                weather = forecast[current_date]
                impact = self.analyzer._calculate_impact(
                    weather,
                    self.analyzer.activities.get(activity.activity_type,
                        self.analyzer.ACTIVITY_TEMPLATES.get('interior_work'))
                )

                productivities.append(impact['productivity'])

                day_info = {
                    'date': current_date,
                    'can_work': impact['can_work'],
                    'productivity': impact['productivity'],
                    'weather': weather.primary_condition.value
                }
                result['day_analysis'].append(day_info)

                if not impact['can_work']:
                    delay_days += 1
                    result['risk_days'].append({
                        'date': current_date,
                        'activity': activity.activity_name,
                        'reason': weather.primary_condition.value
                    })
                elif impact['productivity'] < 0.7:
                    delay_days += (1 - impact['productivity'])

            current_date += timedelta(days=1)

        result['expected_delay'] = round(delay_days)
        result['avg_productivity'] = sum(productivities) / len(productivities) if productivities else 1.0

        return result

    def suggest_reschedule(self, activity: ScheduledActivity,
                          location: Tuple[float, float],
                          flexibility_days: int = 7) -> Optional[date]:
        """Suggest better start date for activity"""
        forecast = self.weather.get_forecast(location[0], location[1])

        best_start = None
        best_avg_productivity = 0

        for offset in range(-flexibility_days, flexibility_days + 1):
            test_start = activity.planned_start + timedelta(days=offset)
            test_end = test_start + timedelta(days=activity.duration_days - 1)

            productivities = []
            for f in forecast:
                if test_start <= f.date <= test_end:
                    act_template = self.analyzer.activities.get(activity.activity_type)
                    if act_template:
                        impact = self.analyzer._calculate_impact(f, act_template)
                        productivities.append(impact['productivity'])

            if productivities:
                avg = sum(productivities) / len(productivities)
                if avg > best_avg_productivity:
                    best_avg_productivity = avg
                    best_start = test_start

        if best_start and best_start != activity.planned_start:
            return best_start
        return None

    def generate_weather_report(self, schedule: List[ScheduledActivity],
                               location: Tuple[float, float],
                               output_path: str) -> str:
        """Generate weather impact report"""
        analysis = self.analyze_schedule(schedule, location)

        with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
            # Summary
            summary = pd.DataFrame([{
                'Total Activities': len(schedule),
                'Weather Delay Days': analysis['weather_delays'],
                'High Risk Days': len(analysis['risk_days']),
                'Recommendations': len(analysis['recommendations'])
            }])
            summary.to_excel(writer, sheet_name='Summary', index=False)

            # Activity details
            activity_data = []
            for act in analysis['activities']:
                activity_data.append({
                    'Activity': act['activity_name'],
                    'Start': act['planned_start'],
                    'End': act['planned_end'],
                    'Avg Productivity': f"{act['avg_productivity']:.0%}",
                    'Expected Delay': f"{act['expected_delay']} days"
                })
            pd.DataFrame(activity_data).to_excel(writer, sheet_name='Activities', index=False)

            # Risk days
            if analysis['risk_days']:
                pd.DataFrame(analysis['risk_days']).to_excel(
                    writer, sheet_name='Risk_Days', index=False
                )

        return output_path
```

## Quick Reference

| Activity Type | Min Temp | Max Precip | Max Wind | Rain Sensitive |
|---------------|----------|------------|----------|----------------|
| Concrete | 5°C | 2mm | 40 km/h | Yes |
| Steel Erection | -10°C | 10mm | 35 km/h | No |
| Roofing | 0°C | 0mm | 30 km/h | Yes |
| Excavation | -5°C | 25mm | 50 km/h | Partial |
| Exterior Painting | 10°C | 0mm | 25 km/h | Yes |
| Masonry | 5°C | 5mm | 40 km/h | Yes |
| Crane Operations | -15°C | 20mm | 30 km/h | No |

## Resources

- **OpenWeatherMap API**: https://openweathermap.org/api
- **Weather Underground**: https://www.wunderground.com/weather/api
- **DDC Website**: https://datadrivenconstruction.io

## Next Steps

- See `4d-simulation` for schedule visualization
- See `risk-assessment-ml` for weather risk prediction
- See `site-logistics-optimization` for delivery scheduling