PV Yield Off by a Factor

On this page

Problem statement

The solar yield estimate is not slightly wrong; it is wrong by a round-looking factor. A 1 kWp system in Toledo produces 6,500 kWh a year, or 7,900, or 1.6 million — against a real figure of about 1,630. Each of those comes from a real mix-up between a rate and an amount, between two units of energy, or between the size of a panel and its rating — and each is easy to make because the numbers involved all look plausible in isolation.

The fix is to know the handful of factors these errors produce, check the result against a sanity range, and carry units explicitly through the calculation.

Quick answer

A fixed, well-oriented PV system produces roughly 800–2,000 kWh per kWp per year depending on latitude (1,600–1,700 in central Spain, 950–1,000 in northern Germany). If a result is outside that range, divide it by the factor that matches:

Factor off Usual cause
× 1,000 W/m² summed and reported as kWh/m², or W treated as kW
× 4 (or × 2, × 12, × 60) sub-hourly values summed as if hourly
× 3.6 (or ÷ 3.6) MJ/m² mixed with kWh/m²
× 4–6 panel area × irradiance with no module efficiency
÷ 8.76 or × 8,760 capacity factor confused with full-load hours
import requests, pandas as pd

j = requests.get("https://archive-api.open-meteo.com/v1/archive", params={
    "latitude": 39.86, "longitude": -4.02,
    "start_date": "2023-01-01", "end_date": "2023-12-31",
    "hourly": "shortwave_radiation", "daily": "shortwave_radiation_sum",
    "timezone": "UTC"}, timeout=120).json()
print("units:", j["hourly_units"]["shortwave_radiation"], "|",
      j["daily_units"]["shortwave_radiation_sum"])

hourly = pd.Series(j["hourly"]["shortwave_radiation"])
daily = pd.Series(j["daily"]["shortwave_radiation_sum"])
print(f"sum of hourly W/m²:         {hourly.sum():>12,.0f}   ← Wh/m², not kWh")
print(f"÷ 1000:                     {hourly.sum() / 1000:>12,.0f}   kWh/m²")
print(f"sum of daily values:        {daily.sum():>12,.0f}   ← MJ/m²")
print(f"÷ 3.6:                      {daily.sum() / 3.6:>12,.0f}   kWh/m²")
units: W/m² | MJ/m²
sum of hourly W/m²:            1,788,058   ← Wh/m², not kWh
÷ 1000:                            1,788   kWh/m²
sum of daily values:               6,437   ← MJ/m²
÷ 3.6:                             1,788   kWh/m²

The same year, two endpoints of the same API, two different units — and neither of them is labelled kWh.

Sanity range, unit fields, intervals, sizing

1. Check the result against the sanity range

Yield per kWp is the most portable number in solar. Compute it before anything else and compare it with 800–2,000 kWh/kWp/year. Irradiation on the tilted plane should sit around 1.3 times the yield (2,106 kWh/m² against 1,633 kWh/kWp in Toledo).

2. Read the unit fields the API returns

Open-Meteo, PVGIS and most weather APIs return units alongside the data. Print them. Hourly Open-Meteo radiation is W/m²; its daily sum is MJ/m²; PVGIS monthly and annual totals are kWh/m² and kWh.

3. Integrate rates with their interval

A W/m² value is a rate. Multiply each value by its interval in hours before summing: 1 for hourly, 0.25 for 15-minute, 1/60 for one-minute data.

4. Size the system by rating, not by area

Energy = irradiation on the plane (kWh/m²) × rating (kWp) × performance ratio (0.75–0.85), because the rating is defined at 1,000 W/m². Using the panel area instead needs the module efficiency too; leaving it out gives a result four to six times too large.

5. Keep capacity factor and full-load hours apart

Full-load hours are kWh per kW. Capacity factor is that divided by 8,760. A capacity factor of 1,633 is a unit error, and so is a yield of 18.6 kWh/kWp.

PV yield errors by the factor they produce, with the cause and the fix for each. the factor names the causeoff bycause → fix× 1,000W/m² summed as kWh/m² → divide by1,000× 4 (15-min data)multiply each value by 0.25 h× 3.6 or ÷ 3.6MJ/m² daily sums → divide by 3.6× 4.8area × irradiance, no efficiency →use kWp× 8,760full-load hours called a capacityfactorSanity range: 800–2,000 kWh/kWp/year for fixed PV spatialworkflow.io
The size of the error usually names its cause.

Sub-hourly sums and missing efficiencies

Example 1 — sub-hourly data summed as hourly

import numpy as np

s = pd.Series(hourly.to_numpy(), index=pd.date_range("2023-01-01", periods=len(hourly),
                                                      freq="h", tz="UTC"))
q = s.resample("15min").interpolate()          # the same year on a 15-minute axis
print(f"hourly, summed:            {s.sum() / 1000:>8,.0f} kWh/m²")
print(f"15-minute, summed:         {q.sum() / 1000:>8,.0f} kWh/m²   ← four times too much")
print(f"15-minute, × 0.25 h:       {(q * 0.25).sum() / 1000:>8,.0f} kWh/m²")
hourly, summed:               1,788 kWh/m²
15-minute, summed:            7,152 kWh/m²   ← four times too much
15-minute, × 0.25 h:          1,788 kWh/m²

The 15-minute series here is the hourly one interpolated, so the correct total is the same by construction; with real 15-minute data it would differ slightly. The factor of four does not.

Example 2 — area times irradiance without an efficiency

irradiation = 2106                    # kWh/m² on a 36° plane, PVGIS, Toledo
kwp = 1.0
module_area = 4.8                     # m² for 1 kWp at 20.8% efficiency
pr = 0.78

wrong = irradiation * module_area * pr
right_rating = irradiation * kwp * pr
right_area = irradiation * module_area * 0.208 * pr
print(f"area × irradiation × PR:        {wrong:>7,.0f} kWh   ← {wrong / right_rating:.1f}× too large")
print(f"kWp × irradiation × PR:         {right_rating:>7,.0f} kWh")
print(f"area × efficiency × irr. × PR:  {right_area:>7,.0f} kWh")
area × irradiation × PR:          7,885 kWh   ← 4.8× too large
kWp × irradiation × PR:           1,643 kWh
area × efficiency × irr. × PR:    1,640 kWh

The rating already contains the efficiency: 1 kWp means 1 kW at 1,000 W/m², so kWh/m² times kWp gives kWh at standard conditions. Either route works; mixing them does not.

Example 3 — a unit test that catches all of these

def check_yield(kwh_per_year, kwp, lat):
    specific = kwh_per_year / kwp
    lo, hi = (700, 1300) if abs(lat) > 50 else (1100, 2300)
    assert lo <= specific <= hi, (
        f"{specific:,.0f} kWh/kWp/year is outside {lo:,}–{hi:,} for latitude {lat}: "
        "check W vs kW, the time step, MJ vs kWh and the area/efficiency")
    return specific

print(check_yield(1_633, 1, 39.86))
try:
    check_yield(6_523, 1, 39.86)
except AssertionError as e:
    print("caught:", e)
1633.0
caught: 6,523 kWh/kWp/year is outside 1,100–2,300 for latitude 39.86: check W vs kW, the time step, MJ vs kWh and the area/efficiency

A range check is crude and sufficient. None of the factor errors in this guide lands inside it.

Annotated PV yield calculation showing where each unit enters and cancels. carry the units and the errors showpoa_kwh_m2 = (poa_w_m2 * dt_h).sum() / 1000energy_kwh = poa_kwh_m2 * kwp * pryield_kwh_per_kwp = energy_kwh / kwpassert 800 <= yield_kwh_per_kwp <= 2300①②③①interval in hours: 1, 0.25, 1/60②rating, defined at 1,000 W/m²③catches every factor errorkWh/m² × kWp gives kWh because the rating already contains the efficiency spatialworkflow.io
Carry the units through the line and every factor error shows up as a unit that does not cancel.

Why the errors are so common

Why the errors are so common

Irradiance (W/m²), irradiation (Wh/m², kWh/m², MJ/m²), power (W, kW, kWp) and energy (Wh, kWh) are four different quantities with overlapping symbols, and most of the arithmetic between them is multiplication by a number close to 1 — 1 hour, 1 kWp, 1,000 W/m². Drop a unit and the arithmetic still runs.

Why MJ/m² survives in weather data

Meteorology measured radiation in MJ/m² per day long before solar engineering standardised on kWh/m². Many climate datasets, reanalysis daily sums and agricultural models still use it. One kWh is 3.6 MJ.

Why a range check beats cleverness

Every factor error in this guide changes the result by at least a factor of three. The plausible range for yield per kWp spans less than a factor of three. A single assertion therefore catches every one of them, which no amount of careful reading of the code guarantees.

Annual results from four unit errors in Toledo against the correct values. Toledo 2023, the same data, five answersGHI, correct (kWh/m²)1,788daily sums read as kWh6,437 — MJ/m² × 115-min sums as hourly7,152 — × 4yield, correct (kWh/kWp)1,643area × irradiance, no efficiency7,885 — × 4.8040008000Every wrong answer is outside the 800–2,000 kWh/kWp sanity range spatialworkflow.io
Each error multiplies the answer by its own factor; none of them lands in the plausible range.

Bifacial systems, labels and night values

  • Bifacial and tracking systems can exceed 2,300 kWh/kWp in the sunniest deserts; widen the range there.
  • kWp vs kWac: a per-kW(AC) yield is 20–40% higher than a per-kWp one for the same plant.
  • Time-ending vs time-starting labels do not change totals but break hourly comparisons.
  • Negative night values from some sensors add a small negative bias if not clipped.
  • Daylight-saving duplicates in local-time data add or remove one hour of production a year.

FAQ

What is a normal annual yield for solar panels?

About 800–2,000 kWh per kWp per year for a fixed, well-oriented system, depending mainly on latitude and cloudiness.

How do I convert W/m² to kWh/m²?

Multiply each value by its time step in hours, sum, and divide by 1,000.

How do I convert MJ/m² to kWh/m²?

Divide by 3.6.

Why is my yield about five times too high?

You probably multiplied irradiation by the panel area without the module efficiency. Use the kWp rating instead.

Why is my yield four times too high with 15-minute data?

You summed 15-minute W/m² values as if they were hourly. Multiply each by 0.25 h first.

How can I catch unit errors automatically?

Assert that the yield per kWp falls within a plausible range for the latitude. Every common unit error falls outside it.