Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ repos:

### Python Tools ###
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.1
rev: v0.16.4
hooks:
- id: ruff-check
args: [--fix]
Expand All @@ -30,15 +30,15 @@ repos:
name: "🐍 python · Format with Ruff"

- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.25
rev: '0.26'
hooks:
- id: validate-pyproject
name: "🐍 python · Validate pyproject.toml"
additional_dependencies: ["validate-pyproject-schema-store[all]"]

### Data & Config Validation ###
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.37.4
rev: 0.38.0
hooks:
- id: check-github-workflows
name: "🐙 github-actions · Validate gh workflow files"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@
SILVER_TABLES_NODES_MAP = {"1c Consumption adjusted levels": "silver_energy_price_cap_annex_9_1c_consumption_adjusted_levels_parquet"}

BENCHMARK_CONSUMPTION = { # MWh per year
"Gas": 11.5,
"Electricity: Single-Rate Metering Arrangement": 2.7,
"Electricity: Multi-Register Metering Arrangement": 3.9,
"Gas": 9.5, # old TDCV before July 26 change was 11.5
"Electricity: Single-Rate Metering Arrangement": 2.5, # old TDCV before July 26 change was 2.7
"Electricity: Multi-Register Metering Arrangement": 3.4, # old TDCV before July 26 change was 3.9
}

VAT = 0.05
Expand Down
74 changes: 51 additions & 23 deletions asf_mission_data/pipeline/energy_price_cap_levels_annex_9/gold.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from asf_mission_data.pipeline.energy_price_cap_levels_annex_9.config import (
BENCHMARK_CONSUMPTION,
COMPONENT_CATEGORY_MAP,
VAT,
)
from asf_mission_data.pipeline.energy_price_cap_levels_annex_9.schemas import (
GOLD_1C_CONSUMPTION_ADJUSTED_LEVELS_WITH_VAT_SCHEMA,
Expand Down Expand Up @@ -73,41 +72,70 @@ def consumption_adjusted_levels_with_vat_df(
"""Add VAT as a tariff component and uprate the total values to include VAT.

This function derives VAT-inclusive tariff values from the silver dataset, and
creates a new tariff component representing VAT (calculated as 5% of the
`Total_GB average` component) and adds it as a separate row. It also uprates
the `Total_GB average` values so that they include VAT.
creates a new tariff component representing VAT (calculated as the difference
between the `Total inc VAT` and `Total_GB average` components) and adds it as
a separate row. It also uprates the `Total_GB average` values so that they
include VAT, using the corresponding `Total inc VAT` values.

Args:
silver_df (pd.DataFrame): Silver-layer Annex 9 DataFrame containing tariff
components, consumption levels, and annual values before VAT
adjustments.
adjustments. Must contain both `Total_GB average` and
`Total inc VAT` tariff components for every fuel.

Returns:
pd.DataFrame: DataFrame containing the original tariff components,
VAT as a separate component, and updated `Total_GB average` values
that include VAT.
pd.DataFrame: DataFrame containing the original tariff components
(excluding `Total inc VAT`), VAT as a separate component, and updated
`Total_GB average` values that include VAT.
"""
for component in ("Total_GB average", "Total inc VAT"):
if not silver_df["Tariff component"].eq(component).any():
raise ValueError(f"Expected tariff component '{component}' not found in silver_df.")

# Add VAT as individual tariff component
if not silver_df["Tariff component"].eq("Total_GB average").any():
raise ValueError("Expected tariff component 'Total_GB average' not found in silver_df.")
# Columns that uniquely identify a row aside from "Tariff component",
# "value" and "metadata"
key_cols = [
"Payment method",
"Fuel",
"Consumption",
"28AD Charge Restriction Period",
"28AD Charge Restriction Period start",
"28AD Charge Restriction Period end",
"28AD Charge Restriction Period interval",
]

vat_rows = silver_df[silver_df["Tariff component"] == "Total_GB average"].copy()
vat_rows["Tariff component"] = "VAT"
vat_rows["value"] *= VAT
total_gb_avg = silver_df[silver_df["Tariff component"] == "Total_GB average"].copy()
total_inc_vat = silver_df[silver_df["Tariff component"] == "Total inc VAT"].copy()

merged = total_gb_avg.merge(
total_inc_vat[key_cols + ["value"]],
on=key_cols,
how="left",
suffixes=("", "_inc_vat"),
validate="one_to_one",
)

# Uprate Total_GB average to include VAT
uprated_silver_df = silver_df.copy()
if merged["value_inc_vat"].isna().any():
raise ValueError("Some 'Total_GB average' rows have no matching 'Total inc VAT' row.")

uprated_silver_df.loc[
(uprated_silver_df["Tariff component"] == "Total_GB average"),
"value",
] *= 1 + VAT
# VAT component = Total inc VAT - Total_GB average
vat_rows = merged.copy()
vat_rows["value"] = vat_rows["value_inc_vat"] - vat_rows["value"]
vat_rows["Tariff component"] = "VAT"
vat_rows = vat_rows.drop(columns="value_inc_vat")

# Uprated Total_GB average = Total inc VAT value
uprated_total_gb_avg = merged.copy()
uprated_total_gb_avg["value"] = uprated_total_gb_avg["value_inc_vat"]
uprated_total_gb_avg = uprated_total_gb_avg.drop(columns="value_inc_vat")

# Remove now redundant "Total inc VAT" rows that were present only in the Dual fuel table
uprated_silver_df = uprated_silver_df[uprated_silver_df["Tariff component"] != "Total inc VAT"]
# All other rows, unchanged (drop original Total_GB average and Total inc VAT rows)
other_rows = silver_df[~silver_df["Tariff component"].isin(["Total_GB average", "Total inc VAT"])].copy()

return pd.concat([uprated_silver_df, vat_rows], ignore_index=True)
return pd.concat(
[other_rows, uprated_total_gb_avg, vat_rows],
ignore_index=True,
)


@check_output(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# %% [markdown]
# ### Checks for TDCV and electricity VAT changes for Oct - Dec 2026 and Jan - Mar 2027 price cap periods

# %%
import pandas as pd

from asf_mission_data import storage
from asf_mission_data.pipeline.energy_price_cap_levels_annex_9.config import (
BENCHMARK_CONSUMPTION,
)

# %%
silver_df = storage.read_parquet(
"s3://asf-mission-data-dev/data/silver/energy_price_cap_levels/annex_9/latest/1c_consumption_adjusted_levels/1c_consumption_adjusted_levels.parquet"
)

# %%
start_date_to_check = "2026-10-01"

# %%
# Levels table checks
gold_levels_df = storage.read_parquet(
"s3://asf-mission-data-dev/data/gold/energy_price_cap_levels/annex_9/latest/1c_consumption_adjusted_levels_with_vat/1c_consumption_adjusted_levels_with_vat.parquet"
)
df = gold_levels_df

# %%
# VAT check, should be zero
df[
(df["28AD Charge Restriction Period start"] == pd.to_datetime(start_date_to_check))
& (df["Consumption"] == "Typical consumption")
& (df["Payment method"] == "Other Payment Method")
& (df["Fuel"] == "Electricity: Single-Rate Metering Arrangement")
& (df["Tariff component"] == "VAT")
]

# %%
# Total bill check, should match final dual fuel bill matches what is published on Ofgem page
df = gold_levels_df
df[
(df["28AD Charge Restriction Period start"] == pd.to_datetime(start_date_to_check))
& (df["Consumption"] == "Typical consumption")
& (df["Payment method"] == "Other Payment Method")
& (df["Fuel"] == "Dual fuel (implied)")
& (df["Tariff component"] == "Total_GB average")
]

# %%
# Electricity bill check
df = gold_levels_df
df[
(df["28AD Charge Restriction Period start"] == pd.to_datetime(start_date_to_check))
& (df["Consumption"] == "Typical consumption")
& (df["Payment method"] == "Other Payment Method")
& (df["Fuel"] == "Electricity: Single-Rate Metering Arrangement")
& (df["Tariff component"] == "Total_GB average")
]

# %%
# Component rates and standing charges check
gold_tariff_component_rates_df = storage.read_parquet(
"s3://asf-mission-data-dev/data/gold/energy_price_cap_levels/annex_9/latest/tariff_component_rates/tariff_component_rates.parquet"
)
df = gold_tariff_component_rates_df

# %%
# Electricity checks
df[
(df["28AD Charge Restriction Period start"] == pd.to_datetime(start_date_to_check))
& (df["Payment method"] == "Other Payment Method")
& (df["Fuel"] == "Electricity: Single-Rate Metering Arrangement")
& (df["Tariff component"] == "Total_GB average")
]

# %%
# Gas checks
df[
(df["28AD Charge Restriction Period start"] == pd.to_datetime(start_date_to_check))
& (df["Payment method"] == "Other Payment Method")
& (df["Fuel"] == "Gas")
& (df["Tariff component"] == "Total_GB average")
]

# %%
# Check annual_bill_fixed_and_variable_contributions_df
# Consumption-based cost (£/yr) should match the unit rate (incl VAT) * TDCV
gold_annual_bill_fixed_and_variable_contributions_df = storage.read_parquet(
"s3://asf-mission-data-dev/data/gold/energy_price_cap_levels/annex_9/latest/annual_bill_fixed_and_variable_component_contributions/annual_bill_fixed_and_variable_component_contributions.parquet"
)
df = gold_annual_bill_fixed_and_variable_contributions_df

# %%
# Electricity checks
df[
(df["28AD Charge Restriction Period start"] == pd.to_datetime(start_date_to_check))
& (df["Payment method"] == "Other Payment Method")
& (df["Fuel"] == "Electricity: Single-Rate Metering Arrangement")
& (df["Tariff component"] == "Total_GB average")
]

# %%
electricity_tdcv = BENCHMARK_CONSUMPTION.get("Electricity: Single-Rate Metering Arrangement") * 1_000 # kWh/year
electricity_unit_rate = 26.322252 # p/kWh
electricity_tdcv * electricity_unit_rate / 100 # should match annual consumption-based cost

# %%
# Gas checks
df[
(df["28AD Charge Restriction Period start"] == pd.to_datetime(start_date_to_check))
& (df["Payment method"] == "Other Payment Method")
& (df["Fuel"] == "Gas")
& (df["Tariff component"] == "Total_GB average")
]

# %%
gas_tdcv = BENCHMARK_CONSUMPTION.get("Gas") * 1_000 # kWh/year
gas_unit_rate = 7.966458 # p/kWh
gas_tdcv * gas_unit_rate / 100 # should match annual consumption-based cost

# %%
Loading