03 — Compare actual years

Learning objectives: compare 2016, leap-year 2020, and 2023 at one point; normalize unequal calendars; and recognize changes in available solar inputs.

Network: SMHI and, if recovery needs it, Open-Meteo. Expected runtime: 5–15 minutes initially. These are actual years, not a trend analysis with only three samples.

[ ]:
from pathlib import Path

import matplotlib.pyplot as plt
import pandas as pd

from smhi2epw import EPWConfig, compile_epw, read_epw

OUTPUT_DIR = Path("examples/output") if Path("examples").exists() else Path("output")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
YEARS = [2016, 2020, 2023]
STATIONS = {2016: 71420, 2020: 72420, 2023: 71420}
LATITUDE, LONGITUDE = 57.7156, 11.9924
[ ]:
frames = {}
diagnostics = []
for year in YEARS:
    path = OUTPUT_DIR / f"gothenburg_{year}.epw"
    result = compile_epw(
        EPWConfig(
            year,
            str(path),
            station_id=STATIONS[year],
            city="Gothenburg",
            latitude=LATITUDE,
            longitude=LONGITUDE,
            utc_offset=1,
        )
    )
    frames[year] = read_epw(path)
    diagnostics.append(
        {
            "year": year,
            "hours": len(frames[year]),
            "station_id": result.station.station_id if result.station else None,
            "weather_classification": result.report.weather_classification,
            "required_reconstructed_fraction": result.report.required_reconstructed_fraction,
            "warning_codes": [w["code"] for w in result.report.warnings],
            "station_distance_km": result.coordinate_distance_km,
            "solar_source": result.report.solar_source,
        }
    )
pd.DataFrame(diagnostics).set_index("year")

Normalize before comparing

2020 contains 8784 hours. Compare mean irradiance or energy per calendar day when the question is typical intensity; retain annual totals when the extra leap day is genuinely part of the load calculation. The current pre-2018 implementation requests only GHI and uses Erbs, although historical STRÅNG DNI is available; direct-horizontal parameter 121 is incomplete for 2017. Göteborg A lacks most 2020 humidity observations, so that year explicitly uses nearby Landvetter (72420); the diagnostics keep this station change visible.

[ ]:
rows = []
for year, frame in frames.items():
    days = len(frame) / 24
    rows.append(
        {
            "year": year,
            "days": days,
            "mean_temperature_C": frame.dry_bulb.mean(),
            "minimum_C": frame.dry_bulb.min(),
            "maximum_C": frame.dry_bulb.max(),
            "GHI_kWh_m2_year": frame.ghi.sum() / 1000,
            "GHI_kWh_m2_day": frame.ghi.sum() / 1000 / days,
        }
    )
annual = pd.DataFrame(rows).set_index("year")
annual.round(2)
[ ]:
fig, ax = plt.subplots(figsize=(9, 4))
for year, frame in frames.items():
    monthly = frame.groupby("month").dry_bulb.mean()
    ax.plot(monthly.index, monthly, marker="o", label=str(year))
ax.set(xlabel="Month", ylabel="Monthly mean dry-bulb temperature (°C)")
ax.legend(title="AMY")
fig.tight_layout()

Key takeaways and exercise

Three historical years can illustrate variability but cannot establish a climate trend. Differences may also include station selection or solar-source changes, so preserve diagnostics.

Try it: compare annual HDD18 totals and HDD18 per day. Which quantity answers an annual energy question, and which is better for comparing unequal calendar lengths?