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.
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.
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.
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.
Internal links
- Solar irradiance explained — rates, totals and their units
- Capacity factor explained — yield, full-load hours and capacity factor
- How to model PV output with pvlib — the chain these units flow through
- Solar timestamps shifted by an hour — the other common solar data bug
- How to download solar irradiance data in Python — reading the unit fields
- Units and scale factors in raster data — the same class of bug in rasters
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.