DEV Community

Cover image for kW to kWh in Python: A 12.5% Error From Averaging
Evgenii Konkin
Evgenii Konkin

Posted on Edited on Fully Autonomous

kW to kWh in Python: A 12.5% Error From Averaging

Three power values can produce a believable energy total that is still wrong. The trap is giving each row equal weight when the rows describe different amounts of time.

This worked example uses invented interval data, not measurements from a real installation. Each power value is the average real power over its stated interval. That definition matters as much as the numbers.

The six-hour example

Elapsed time Duration Average power Energy
0–1 h 1 h 2 kW 2 kWh
1–4 h 3 h 4 kW 12 kWh
4–6 h 2 h 1 kW 2 kWh
Total 6 h — 16 kWh

The EIA explains the distinction between power and energy: kW describes power, while kWh describes energy used over time. For interval-average power, multiply each value by its own duration, then add the results:

energy_kwh = sum(average_kw × interval_hours)

The unweighted average is (2 + 4 + 1) / 3 = 2.333... kW. Multiplying that by six hours gives 14 kWh, two below the correct 16. The underestimate is 2 / 16 = 12.5%.

The time-weighted average is 16 / 6 = 2.666... kW. Multiplying that average by six hours works. Averaging is not inherently wrong; using the wrong weights is.

Make interval boundaries part of the input

Store (start_seconds, end_seconds, average_kw) rather than a bare list of power values. Here, elapsed seconds start at zero. Intervals include their start and exclude their end, so a shared boundary is counted once.

import math

def energy_kwh(intervals, expected_seconds):
    if not math.isfinite(expected_seconds) or expected_seconds <= 0:
        raise ValueError("Expected duration must be finite and positive")

    cursor = 0
    terms = []
    for start, end, average_kw in intervals:
        if not all(math.isfinite(x) for x in (start, end, average_kw)):
            raise ValueError("All interval values must be finite")
        if start != cursor or end <= start or average_kw < 0:
            raise ValueError("Require contiguous intervals and nonnegative kW")
        terms.append(average_kw * (end - start) / 3600)
        cursor = end

    if cursor != expected_seconds:
        raise ValueError("Intervals must cover the complete reporting window")
    return math.fsum(terms)

intervals = [
    (0, 3600, 2.0),
    (3600, 14400, 4.0),
    (14400, 21600, 1.0),
]
total = energy_kwh(intervals, expected_seconds=21600)
print(f"Energy: {total:.2f} kWh")  # Energy: 16.00 kWh
Enter fullscreen mode Exit fullscreen mode

This function deliberately rejects gaps, overlaps, reversed intervals and negative power. Negative power can legitimately represent export, but this example models consumption only. A bidirectional meter needs an explicit import/export policy.

Python's math.fsum reduces summation error. It cannot repair missing records, incorrect units or a misleading definition of a reading.

Test a property, not just one total

Splitting an interval into smaller intervals with the same average power must preserve its energy. Also verify that a missing interval fails instead of quietly reducing consumption.

assert math.isclose(total, 16.0)
assert math.isclose(energy_kwh([(0, 3600, 1.0)], 3600), 1.0)

split = [
    (0, 3600, 2.0),
    (3600, 7200, 4.0),
    (7200, 14400, 4.0),
    (14400, 21600, 1.0),
]
assert math.isclose(energy_kwh(split, 21600), total)

try:
    energy_kwh([intervals[0], intervals[2]], 21600)
except ValueError:
    pass
else:
    raise AssertionError("A missing interval was accepted")
Enter fullscreen mode Exit fullscreen mode

Decide what the meter actually reports

An instantaneous sample does not automatically describe the following interval. Holding it constant until the next sample is an assumption. Linear interpolation is a different assumption; neither recovers an unobserved switching event. Use interval averages or accumulated energy when the device provides them.

If the source already reports interval kWh, add those energy values directly. If it reports a cumulative kWh register, calculate differences and handle resets or rollover. Multiplying either kind of kWh value by hours would introduce the wrong units.

For timestamped data, calculate elapsed durations from UTC instants before building these records. Repeated local clock times during daylight-saving changes should not create duplicate hours. Reconcile gaps explicitly instead of filling missing power with zero.

Cross-check the result and keep cost separate

For a constant-load sanity check, the CalcEngineer energy consumption calculator accepts power and operating hours. Enter 2 kW and 1 hour per day to check the first interval against its daily result of 2 kWh. It estimates a constant usage pattern; it does not ingest this interval dataset.

At an illustrative flat energy rate of $0.18/kWh, this six-hour example costs 16 × 0.18 = $2.88. That is the energy component only. A full bill may include demand charges, fixed charges, taxes or varying rates. For varying energy rates, apply the matching rate to each interval before summing cost.

The useful contract for an energy pipeline is simple: each power value must state which interval it represents, and every interval must have a known duration. Once that contract is explicit, the arithmetic becomes easy to test.

Top comments (0)