How to Estimate Wind Energy with a Weibull Distribution

On this page

Problem statement

Wind atlases and site reports summarise a year of wind as two numbers: the Weibull scale c and shape k. A turbine manufacturer supplies a power curve. Between them sits the annual energy estimate, and the calculation is simple once it is set up properly: integrate the power curve against the distribution. The errors come from doing something simpler instead — reading the power at the mean speed, or fitting the distribution badly at the low end, or forgetting that the curve was published for sea-level air.

This guide fits a Weibull to a year of hub-height wind at two sites, computes the energy three ways, and checks what the distribution gets right and wrong against the hourly data it came from.

Quick answer

Fit k and c, then integrate the power curve against the density:

import requests, numpy as np, pandas as pd
from scipy import stats
from windpowerlib import WindTurbine

turbine = WindTurbine(turbine_type="V126/3450", hub_height=117)
curve_v = turbine.power_curve.wind_speed.to_numpy()
curve_p = turbine.power_curve.value.to_numpy() / 1e6          # MW

def hub_speed(lat, lon, start="2023-01-01", end="2023-12-31"):
    j = requests.get("https://archive-api.open-meteo.com/v1/archive", params={
        "latitude": lat, "longitude": lon, "start_date": start, "end_date": end,
        "hourly": "wind_speed_10m,wind_speed_100m", "wind_speed_unit": "ms",
        "timezone": "UTC"}, timeout=300).json()
    d = pd.DataFrame(j["hourly"])
    d.index = pd.to_datetime(d.pop("time"))
    alpha = np.log(d.wind_speed_100m.mean() / d.wind_speed_10m.mean()) / np.log(10)
    return d.wind_speed_100m * (117 / 100) ** alpha

def weibull_energy(k, c, hours=8760):
    v = np.linspace(0, 30, 3001)
    pdf = stats.weibull_min.pdf(v, k, scale=c)
    power = np.interp(v, curve_v, curve_p, right=0)          # 0 above cut-out
    return np.trapezoid(pdf * power, v) * hours               # MWh

v = hub_speed(56.09, 8.24)                                    # western Jutland
k, _, c = stats.weibull_min.fit(v[v > 0], floc=0)
e_weibull = weibull_energy(k, c)
e_hourly = np.interp(v, curve_v, curve_p, right=0).sum()
print(f"Weibull fit: k = {k:.2f}, c = {c:.2f} m/s")
print(f"energy from the Weibull: {e_weibull:,.0f} MWh "
      f"(capacity factor {e_weibull / (3.45 * 8760):.1%})")
print(f"energy from the hours:   {e_hourly:,.0f} MWh "
      f"({e_weibull / e_hourly - 1:+.1%} difference)")
Weibull fit: k = 2.19, c = 9.72 m/s
energy from the Weibull: 16,709 MWh (capacity factor 55.3%)
energy from the hours:   16,648 MWh (+0.4% difference)

The two agree to 0.4% here: a good Weibull fit carries the energy information of 8,760 hourly values in two numbers.

Hub-height speeds, a zero-location fit, a check at the curve

1. Get hub-height speeds, not 10 m ones

The fit should be done at the height where the energy is produced, after shear extrapolation — not at 10 m and then scaled, because the shape parameter changes with height.

2. Fit by maximum likelihood with the location fixed at zero

scipy.stats.weibull_min.fit(v, floc=0) returns (k, 0, c). Leaving the location free produces a three-parameter fit that describes the data slightly better and cannot be compared with any atlas.

3. Check the fit where the power curve is steep

Between 5 and 12 m/s every error in the distribution becomes an error in energy. Compare the fitted and observed frequencies there, not just the overall goodness of fit.

4. Integrate against the power curve on a fine grid

Use a speed step of 0.01 m/s and make sure the curve returns zero above cut-out. A coarse integration or an extrapolated curve adds a percent of error for no reason.

5. Correct for air density

Scale the speeds by (ρ / 1.225) ** (1/3) before the power curve — the standard IEC method for pitch-regulated turbines.

6. Report the year-to-year spread

A single year's Weibull is a single year's wind. Fit several years or use a long-term correction, and state the interannual variability.

Share of hours and share of energy by wind speed band for a V126-3.45 in western Jutland in 2023. where the year’s energy comes fromhub-height speedshare of hoursshare of energy0–3 m/s7%0%3–6 m/s21%4%6–9 m/s30%24%9–12 m/s23%38%12–15 m/s11%20%15+ m/s8%14% (capped)Weibull k 2.19, c 9.72 m/s — energy from the fit within 0.4% of the hourly sum6–12 m/s: half the hours, 62% of the energy spatialworkflow.io
The energy comes from the overlap: frequent speeds times the power they produce.

Which speeds produce the energy

Example 1 — which speeds produce the energy

bins = np.arange(0, 27, 3)
power = np.interp(v, curve_v, curve_p, right=0)
by_bin = pd.DataFrame({"hours": v.groupby(pd.cut(v, bins)).size(),
                       "energy": pd.Series(power, index=v.index).groupby(pd.cut(v, bins)).sum()})
by_bin["share of hours"] = by_bin.hours / by_bin.hours.sum()
by_bin["share of energy"] = by_bin.energy / by_bin.energy.sum()
print(by_bin[["share of hours", "share of energy"]].map(lambda x: f"{x:.0%}").to_string())
                share of hours share of energy
wind_speed_100m                               
(0, 3]                      7%              0%
(3, 6]                     21%              4%
(6, 9]                     30%             24%
(9, 12]                    23%             38%
(12, 15]                   11%             20%
(15, 18]                    6%             10%
(18, 21]                    2%              4%
(21, 24]                    0%              0%

The 6–12 m/s range is half of the hours and 62% of the energy. Hours below 3 m/s produce nothing, and the hours above 12 m/s produce the rated output and no more.

Example 2 — mean speed, Weibull and hourly, at two sites

print(f"{'site':<12}{'mean v':>7}{'k':>6}{'hours':>9}{'Weibull':>9}{'mean-v':>9}")
for name, (lat, lon) in {"Ringkøbing": (56.09, 8.24), "Albacete": (38.99, -1.86)}.items():
    s = hub_speed(lat, lon)
    kk, _, cc = stats.weibull_min.fit(s[s > 0], floc=0)
    cf = lambda e: e / (3.45 * 8760)
    hourly = np.interp(s, curve_v, curve_p, right=0).sum()
    at_mean = np.interp(s.mean(), curve_v, curve_p) * 8760
    print(f"{name:<12}{s.mean():>7.2f}{kk:>6.2f}{cf(hourly):>9.1%}"
          f"{cf(weibull_energy(kk, cc)):>9.1%}{cf(at_mean):>9.1%}")
site         mean v     k    hours  Weibull   mean-v
Ringkøbing     8.61  2.19    55.1%    55.3%    63.4%
Albacete       5.30  1.83    24.7%    24.3%    14.2%

Reading the power curve at the mean speed is not a conservative shortcut. It overestimates at the windy site, where the mean sits on the steep part of the curve and the spread above rated is thrown away, and badly underestimates at the calm one, where the mean sits near cut-in and the energy comes from the tail.

Example 3 — ten years of the same site

v10y = hub_speed(56.09, 8.24, "2014-01-01", "2023-12-31")
yearly = v10y.groupby(v10y.index.year).apply(
    lambda s: np.interp(s, curve_v, curve_p, right=0).sum() / (3.45 * len(s)))
print(yearly.map(lambda x: f"{x:.1%}").to_string())
print(f"mean {yearly.mean():.1%}, std {yearly.std():.1%} "
      f"(relative {yearly.std() / yearly.mean():.1%})")
time
2014    62.1%
2015    65.1%
2016    61.0%
2017    58.4%
2018    55.2%
2019    58.5%
2020    58.0%
2021    51.3%
2022    58.4%
2023    54.8%
mean 58.3%, std 3.9% (relative 6.7%)

One year is not the resource: the gross capacity factor ranges from 51.3% in 2021 to 65.1% in 2015 at the same grid cell. A project financed on the windiest year of the decade would miss its numbers in every other one.

Gross capacity factor by year at a western Jutland grid cell from 2014 to 2023. gross capacity factor, V126 at 117 m201462.1%201565.1% — windiest201661.0%201758.4%201855.2%201958.5%202058.0%202151.3% — calmest202258.4%202354.8%035%70%Mean 58.3%, standard deviation 3.9 points (6.7% relative) spatialworkflow.io
Ten years at one grid cell: the spread is the uncertainty a single year hides.

Why the Weibull works, and the mean fails

Why the Weibull works as well as it does

Wind speed is the magnitude of a two-dimensional velocity whose components are roughly normal; that gives a Rayleigh distribution, which is a Weibull with k = 2. Real sites deviate — k from about 1.5 to 3 — and the two-parameter Weibull absorbs most of that deviation. It fails at sites with two regimes (a sea breeze plus synoptic winds, for example), where a mixture fits better.

Why the mean speed fails in both directions

The power curve is convex below rated and concave above. When most of the distribution sits on the convex part, Jensen's inequality says the power at the mean is less than the mean power; when a lot of it sits above rated, the reverse. The error has no fixed sign, so it cannot be corrected with a factor.

Why density goes in as a speed correction

For a pitch-regulated turbine, lower density means the rotor needs a higher speed to reach the same power. Scaling the speed by the cube root of the density ratio shifts the curve along the speed axis, which is closer to the truth than scaling the power.

Choosing the wind energy method from the data available: hourly speeds, Weibull parameters or only a mean. use the most distribution you haveWhat wind data do you have at hubheight?hourly speedspower curve per hourthen averageWeibull k and cintegrate curve × pdfwithin ~1% herea mean speedRayleigh, k = 2with a stated caveatNever read the power curve at the mean speed spatialworkflow.io
The method follows from how much of the distribution survived into your data.

Calm hours, sectors and reanalysis bias

  • Calm hours (v = 0) must be dropped before a Weibull fit with floc=0, or the likelihood is undefined.
  • Sector-wise Weibulls (12 × 30°) are standard for layout and wake modelling.
  • Cut-out hysteresis: above 22–25 m/s turbines stop and restart only below a lower speed; a simple curve slightly overestimates energy in storms.
  • Reanalysis bias: ERA5 tends to underestimate high speeds over complex terrain; long-term correction against a local mast (MCP) is standard for bankable estimates.
  • Net energy: subtract wakes, availability and electrical losses — typically 10–20% — before calling anything a yield.

FAQ

How do I calculate wind energy from Weibull parameters?

Integrate the turbine's power curve multiplied by the Weibull probability density over wind speed, and multiply by the hours in the period.

What do the Weibull k and c mean?

c is the scale, close to 1.13 times the mean speed for typical k; k is the shape — lower k means a wider spread of speeds.

Can I use the mean wind speed with the power curve?

No. The error can be large and in either direction.

How good is a Weibull fit?

Usually within a few percent of the energy computed from the hourly data at the same site.

Should I fit the Weibull at 10 m or at hub height?

At hub height. The shape changes with height.

How much does annual wind energy vary?

Typically a few percent to over 10% standard deviation between years; compute it from as many years as the data allow.