energykit.cost

Translate energy data into financial impact: demand charges, imbalance costs.

energykit.cost — Translate energy data and forecast errors into dollars.

Modules

demand_charge Identify and quantify demand charge events. Simulate battery savings. imbalance Compute imbalance settlement costs from forecast errors.

class energykit.cost.DemandChargeAnalyzer(demand_rate: float = 12.5, peak_hours: List[int] | None = None, billing_period: str = 'ME', dt_hours: float = 1.0)[source]

Bases: object

Analyse demand charges from power time series data.

Identifies the peak demand event in each billing period, calculates the resulting demand charge, and simulates the savings achievable with different battery peak-shaving configurations.

Parameters:
  • demand_rate (float, default 12.50) – Demand charge rate in $/kW/month (applied to the highest kW in each billing period).

  • peak_hours (list of int or None) – Hours (0–23) during which demand charges apply. Use this for on-peak demand riders. None = all hours (flat demand charge).

  • billing_period (str, default "ME") – Pandas resample frequency string for billing periods. "ME" = month end, "QE" = quarter end.

  • dt_hours (float, default 1.0) – Timestep duration in hours. Set to 0.25 for 15-minute interval data. Does not affect demand charge calculation (which is peak kW, not kWh) but is used for battery energy feasibility checks.

Examples

>>> analyzer = DemandChargeAnalyzer(demand_rate=15.0, peak_hours=list(range(9, 22)))
>>> result = analyzer.analyze(power_series)
>>> result.battery_savings_df
analyze(power_kw: Series) DemandChargeResult[source]

Analyse demand charges in a power time series.

Parameters:

power_kw (pd.Series) – Power demand in kW with a DatetimeIndex. Sub-hourly (15-min) resolution gives the most accurate demand charge estimates.

Return type:

DemandChargeResult

class energykit.cost.DemandChargeResult(peak_events_df: DataFrame, peak_events: List[PeakEvent], total_annual_charge_usd: float, worst_event: PeakEvent, battery_savings_df: DataFrame, n_periods: int)[source]

Bases: object

Results from DemandChargeAnalyzer.

peak_events_df

One row per billing period with peak_kw, peak_timestamp, charge_usd.

Type:

pd.DataFrame

peak_events

Structured list for programmatic access.

Type:

list of PeakEvent

total_annual_charge_usd

Sum of all billing period demand charges over the analysed window.

Type:

float

worst_event

The single most expensive peak event.

Type:

PeakEvent

battery_savings_df

Battery sizing → annual savings table.

Type:

pd.DataFrame

n_periods

Number of billing periods analysed.

Type:

int

battery_savings_df: DataFrame
n_periods: int
peak_events: List[PeakEvent]
peak_events_df: DataFrame
total_annual_charge_usd: float
worst_event: PeakEvent
class energykit.cost.ImbalanceCostCalculator(imbalance_price: float = 0.08, imbalance_price_up: float | None = None, imbalance_price_down: float | None = None, dt_hours: float = 1.0)[source]

Bases: object

Calculate imbalance settlement costs from forecast vs actual series.

Models the financial cost of forecast errors in energy markets. Uses a symmetric imbalance price by default (any deviation is penalised equally), or an asymmetric model where over- and under-forecasting have different prices.

Parameters:
  • imbalance_price (float) – Symmetric imbalance price in $/kWh (applied to absolute error). Typical values: $0.05–$0.15/kWh for retail; $0.02–$0.08/kWh wholesale.

  • imbalance_price_up (float or None) – Price for positive imbalance (you consumed/produced MORE than forecast). If None, uses imbalance_price for both directions.

  • imbalance_price_down (float or None) – Price for negative imbalance (you consumed/produced LESS than forecast). If None, uses imbalance_price for both directions.

  • dt_hours (float, default 1.0) – Timestep duration in hours. Multiply energy error (kW × dt) → kWh.

Examples

>>> calc = ImbalanceCostCalculator(imbalance_price=0.08)
>>> result = calc.compute(forecast, actual)
>>> print(result)
compute(forecast, actual, index: DatetimeIndex | None = None) ImbalanceResult[source]

Compute imbalance costs from forecast and actual series.

Parameters:
  • forecast (array-like or pd.Series) – Forecasted energy / power values (kW or kWh).

  • actual (array-like or pd.Series) – Observed values (same units as forecast).

  • index (pd.DatetimeIndex or None) – Datetime index for the series. Required for temporal breakdowns. Inferred from actual if it is a pd.Series.

Return type:

ImbalanceResult

energykit.cost.forecast_value_of_accuracy(actual, forecast, imbalance_price: float = 0.08, dt_hours: float = 1.0, target_mape_pct: float | None = None, index: DatetimeIndex | None = None) ForecastValueReport[source]

Quantify the annual financial value of improving forecast accuracy.

This function answers: “How much is each percentage point of MAPE improvement worth in dollars per year?”

The model is linear: imbalance cost ∝ MAPE. This is an approximation — the true relationship depends on the price distribution and error distribution — but it is a solid first-order estimate used in practice by trading desks for investment decisions.

Parameters:
  • actual (array-like or pd.Series) – Observed values.

  • forecast (array-like or pd.Series) – Forecasted values.

  • imbalance_price (float) – Imbalance settlement price in $/kWh (default: $0.08/kWh).

  • dt_hours (float) – Timestep duration in hours.

  • target_mape_pct (float or None) – The improvement target. Defaults to half the current MAPE.

  • index (pd.DatetimeIndex or None) – Datetime index for annualisation.

Returns:

Includes a full improvement table and the “break-even investment” number.

Return type:

ForecastValueReport

Examples

>>> report = forecast_value_of_accuracy(actual, forecast, imbalance_price=0.10)
>>> print(report)

Demand Charge Analyzer

energykit.cost.demand_charge

Demand charge analysis and battery peak-shaving simulation.

Background

Most commercial and industrial electricity tariffs include a demand charge: a fee based on the single highest power demand in a billing period (usually the peak 15-minute or 1-hour interval in a month). This charge can represent 30–70% of a large customer’s electricity bill yet typically stems from just a handful of short events per year.

A single HVAC unit switching on at the wrong time can add $10,000 to a monthly bill. DemandChargeAnalyzer identifies exactly when those events happened, how much they cost, and what battery capacity would have prevented them.

Usage

>>> from energykit.cost import DemandChargeAnalyzer
>>>
>>> analyzer = DemandChargeAnalyzer(demand_rate=12.50)   # $/kW/month
>>> result = analyzer.analyze(power_kw_series)
>>>
>>> print(result.peak_events_df)
>>>      period  peak_kw         peak_timestamp  demand_charge_usd
>>>   2025-01    5.23  2025-01-15 17:00:00              65.38
>>>   2025-02    4.81  2025-02-08 18:30:00              60.13
>>>   ...
>>>
>>> print(result.battery_savings_df)
>>>   battery_kwh  max_power_kw  annual_savings_usd  pct_reduction
>>>           5.0           2.5               97.50           13.2
>>>          10.0           5.0              186.25           25.2
>>>          20.0          10.0              312.50           42.3
>>>          50.0          25.0              502.50           68.0
class energykit.cost.demand_charge.DemandChargeAnalyzer(demand_rate: float = 12.5, peak_hours: List[int] | None = None, billing_period: str = 'ME', dt_hours: float = 1.0)[source]

Bases: object

Analyse demand charges from power time series data.

Identifies the peak demand event in each billing period, calculates the resulting demand charge, and simulates the savings achievable with different battery peak-shaving configurations.

Parameters:
  • demand_rate (float, default 12.50) – Demand charge rate in $/kW/month (applied to the highest kW in each billing period).

  • peak_hours (list of int or None) – Hours (0–23) during which demand charges apply. Use this for on-peak demand riders. None = all hours (flat demand charge).

  • billing_period (str, default "ME") – Pandas resample frequency string for billing periods. "ME" = month end, "QE" = quarter end.

  • dt_hours (float, default 1.0) – Timestep duration in hours. Set to 0.25 for 15-minute interval data. Does not affect demand charge calculation (which is peak kW, not kWh) but is used for battery energy feasibility checks.

Examples

>>> analyzer = DemandChargeAnalyzer(demand_rate=15.0, peak_hours=list(range(9, 22)))
>>> result = analyzer.analyze(power_series)
>>> result.battery_savings_df
analyze(power_kw: Series) DemandChargeResult[source]

Analyse demand charges in a power time series.

Parameters:

power_kw (pd.Series) – Power demand in kW with a DatetimeIndex. Sub-hourly (15-min) resolution gives the most accurate demand charge estimates.

Return type:

DemandChargeResult

class energykit.cost.demand_charge.DemandChargeResult(peak_events_df: DataFrame, peak_events: List[PeakEvent], total_annual_charge_usd: float, worst_event: PeakEvent, battery_savings_df: DataFrame, n_periods: int)[source]

Bases: object

Results from DemandChargeAnalyzer.

peak_events_df

One row per billing period with peak_kw, peak_timestamp, charge_usd.

Type:

pd.DataFrame

peak_events

Structured list for programmatic access.

Type:

list of PeakEvent

total_annual_charge_usd

Sum of all billing period demand charges over the analysed window.

Type:

float

worst_event

The single most expensive peak event.

Type:

PeakEvent

battery_savings_df

Battery sizing → annual savings table.

Type:

pd.DataFrame

n_periods

Number of billing periods analysed.

Type:

int

battery_savings_df: DataFrame
n_periods: int
peak_events: List[PeakEvent]
peak_events_df: DataFrame
total_annual_charge_usd: float
worst_event: PeakEvent
class energykit.cost.demand_charge.PeakEvent(period: str, timestamp: Timestamp, peak_kw: float, demand_charge_usd: float)[source]

Bases: object

A single billing-period peak demand event.

period

Billing period label (e.g. "2025-01").

Type:

str

timestamp

Exact timestamp of the peak reading.

Type:

pd.Timestamp

peak_kw

Peak power demand in kW.

Type:

float

demand_charge_usd

Demand charge for this billing period (USD).

Type:

float

demand_charge_usd: float
peak_kw: float
period: str
timestamp: Timestamp

Imbalance Cost Calculator

energykit.cost.imbalance

Imbalance settlement cost calculation and forecast value quantification.

Background

In deregulated energy markets (EPEX-SPOT, Nord Pool, PJM, CAISO, etc.), market participants submit day-ahead schedules / bids. Deviations from the scheduled quantity in real time are settled at the imbalance price — which is almost always more expensive than the day-ahead price, creating a direct financial penalty for forecast errors.

For a portfolio manager:
  • Positive imbalance (produced/consumed more than forecast): settled at a typically unfavourable price (curtailment or spill cost).

  • Negative imbalance (underproduced / under-consumed): settled at the imbalance price which may be 2–5× the day-ahead price during scarcity.

ImbalanceCostCalculator maps a forecast error time series directly to a settlement cost. forecast_value_of_accuracy answers the question every energy manager asks: “How much is it worth to improve our MAPE from 8% to 4%?”

Usage

>>> from energykit.cost import ImbalanceCostCalculator, forecast_value_of_accuracy
>>>
>>> calc = ImbalanceCostCalculator(imbalance_price=0.08)  # $80/MWh → $0.08/kWh
>>> result = calc.compute(forecast=my_forecast, actual=actual_load)
>>>
>>> print(f"Imbalance cost this month: ${result.total_cost_usd:.2f}")
>>> print(result.cost_by_hour_df)    # which hours of day are most expensive
>>> print(result.top_errors_df)      # the 10 most costly individual errors
>>>
>>> # Value of accuracy improvement
>>> report = forecast_value_of_accuracy(actual, forecast, imbalance_price=0.08)
>>> print(report)
>>> # Current MAPE 7.2% costs $234,000/yr in imbalance settlement.
>>> # Improving to 3.6% MAPE saves $117,000/yr.
class energykit.cost.imbalance.ForecastValueReport(current_mape_pct: float, current_annual_cost_usd: float, target_mape_pct: float, target_annual_cost_usd: float, potential_annual_savings_usd: float, cost_per_mape_pct_usd: float, breakeven_investment_usd: float, improvement_table: DataFrame)[source]

Bases: object

Output of forecast_value_of_accuracy().

current_mape_pct
Type:

float

current_annual_cost_usd

Estimated annual imbalance cost at current MAPE.

Type:

float

target_mape_pct

The improvement target (default: half of current MAPE).

Type:

float

target_annual_cost_usd

Estimated annual cost at the target MAPE.

Type:

float

potential_annual_savings_usd

Savings from reaching the target MAPE.

Type:

float

cost_per_mape_pct_usd

Dollar value of each 1% point of MAPE improvement per year.

Type:

float

breakeven_investment_usd

How much a forecasting system could cost and break even in 1 year.

Type:

float

improvement_table

Columns: target_mape_pct, annual_cost_usd, annual_savings_usd.

Type:

pd.DataFrame

breakeven_investment_usd: float
cost_per_mape_pct_usd: float
current_annual_cost_usd: float
current_mape_pct: float
improvement_table: DataFrame
potential_annual_savings_usd: float
target_annual_cost_usd: float
target_mape_pct: float
class energykit.cost.imbalance.ImbalanceCostCalculator(imbalance_price: float = 0.08, imbalance_price_up: float | None = None, imbalance_price_down: float | None = None, dt_hours: float = 1.0)[source]

Bases: object

Calculate imbalance settlement costs from forecast vs actual series.

Models the financial cost of forecast errors in energy markets. Uses a symmetric imbalance price by default (any deviation is penalised equally), or an asymmetric model where over- and under-forecasting have different prices.

Parameters:
  • imbalance_price (float) – Symmetric imbalance price in $/kWh (applied to absolute error). Typical values: $0.05–$0.15/kWh for retail; $0.02–$0.08/kWh wholesale.

  • imbalance_price_up (float or None) – Price for positive imbalance (you consumed/produced MORE than forecast). If None, uses imbalance_price for both directions.

  • imbalance_price_down (float or None) – Price for negative imbalance (you consumed/produced LESS than forecast). If None, uses imbalance_price for both directions.

  • dt_hours (float, default 1.0) – Timestep duration in hours. Multiply energy error (kW × dt) → kWh.

Examples

>>> calc = ImbalanceCostCalculator(imbalance_price=0.08)
>>> result = calc.compute(forecast, actual)
>>> print(result)
compute(forecast, actual, index: DatetimeIndex | None = None) ImbalanceResult[source]

Compute imbalance costs from forecast and actual series.

Parameters:
  • forecast (array-like or pd.Series) – Forecasted energy / power values (kW or kWh).

  • actual (array-like or pd.Series) – Observed values (same units as forecast).

  • index (pd.DatetimeIndex or None) – Datetime index for the series. Required for temporal breakdowns. Inferred from actual if it is a pd.Series.

Return type:

ImbalanceResult

class energykit.cost.imbalance.ImbalanceResult(total_cost_usd: float, annual_cost_estimate_usd: float, cost_by_hour_df: DataFrame, cost_by_dow_df: DataFrame, cost_by_month_df: DataFrame, top_errors_df: DataFrame, current_mape_pct: float, cost_per_mape_pct_usd: float)[source]

Bases: object

Output of ImbalanceCostCalculator.

total_cost_usd

Total imbalance settlement cost over the input period.

Type:

float

annual_cost_estimate_usd

Annualised cost (extrapolated from the input period length).

Type:

float

cost_by_hour_df

Mean hourly imbalance cost. Reveals which times of day drive losses.

Type:

pd.DataFrame

cost_by_dow_df

Mean daily imbalance cost by day of week.

Type:

pd.DataFrame

cost_by_month_df

Monthly cost totals.

Type:

pd.DataFrame

top_errors_df

The 20 individual timesteps with the highest imbalance cost.

Type:

pd.DataFrame

current_mape_pct

MAPE between forecast and actual (%).

Type:

float

cost_per_mape_pct_usd

Annual cost per 1% MAPE. Use this as the “price tag” of forecast error.

Type:

float

annual_cost_estimate_usd: float
cost_by_dow_df: DataFrame
cost_by_hour_df: DataFrame
cost_by_month_df: DataFrame
cost_per_mape_pct_usd: float
current_mape_pct: float
top_errors_df: DataFrame
total_cost_usd: float
energykit.cost.imbalance.forecast_value_of_accuracy(actual, forecast, imbalance_price: float = 0.08, dt_hours: float = 1.0, target_mape_pct: float | None = None, index: DatetimeIndex | None = None) ForecastValueReport[source]

Quantify the annual financial value of improving forecast accuracy.

This function answers: “How much is each percentage point of MAPE improvement worth in dollars per year?”

The model is linear: imbalance cost ∝ MAPE. This is an approximation — the true relationship depends on the price distribution and error distribution — but it is a solid first-order estimate used in practice by trading desks for investment decisions.

Parameters:
  • actual (array-like or pd.Series) – Observed values.

  • forecast (array-like or pd.Series) – Forecasted values.

  • imbalance_price (float) – Imbalance settlement price in $/kWh (default: $0.08/kWh).

  • dt_hours (float) – Timestep duration in hours.

  • target_mape_pct (float or None) – The improvement target. Defaults to half the current MAPE.

  • index (pd.DatetimeIndex or None) – Datetime index for annualisation.

Returns:

Includes a full improvement table and the “break-even investment” number.

Return type:

ForecastValueReport

Examples

>>> report = forecast_value_of_accuracy(actual, forecast, imbalance_price=0.10)
>>> print(report)