Source code for apem.unit_based_model.evaluation.redispatch_analysis
"""Utilities for loading and validating redispatch cost and volume metrics."""
from __future__ import annotations
from pathlib import Path
import pandas as pd
REQUIRED_COLUMNS = ("redispatch_algorithm", "metric", "value")
SUPPORTED_METRICS = {"costs", "volumes"}
REDISPATCH_FILE_PATTERNS = {
"costs": "_redispatch_costs.csv",
"volumes": "_redispatch_vols.csv",
}
[docs]
def validate_redispatch_table(df: pd.DataFrame) -> pd.DataFrame:
"""
Validate and normalize a generic redispatch-analysis input table.
:param df: input table expected to contain ``redispatch_algorithm``,
``metric``, and ``value``
:return: normalized copy with lowercase metric labels and numeric ``value``
:raises ValueError: if required columns are missing, metric values are
unsupported, algorithm labels are empty, or no numeric
values are available
"""
normalized = df.copy()
normalized.columns = [str(column).strip() for column in normalized.columns]
missing = [column for column in REQUIRED_COLUMNS if column not in normalized.columns]
if missing:
raise ValueError(f"Missing required columns: {missing}. Required columns: {list(REQUIRED_COLUMNS)}.")
normalized["redispatch_algorithm"] = normalized["redispatch_algorithm"].astype(str).str.strip()
normalized["metric"] = normalized["metric"].astype(str).str.strip().str.lower()
normalized["value"] = pd.to_numeric(normalized["value"], errors="coerce")
if normalized["redispatch_algorithm"].eq("").any():
raise ValueError("Column 'redispatch_algorithm' contains empty values.")
invalid_metrics = sorted(set(normalized["metric"]) - SUPPORTED_METRICS)
if invalid_metrics:
raise ValueError(
f"Unsupported metric values: {invalid_metrics}. Supported metrics: {sorted(SUPPORTED_METRICS)}."
)
if normalized["value"].notna().sum() == 0:
raise ValueError("Column 'value' does not contain any numeric values.")
return normalized
[docs]
def load_redispatch_metric_file(
path: str | Path,
*,
redispatch_algorithm: str | None = None,
metric: str | None = None,
) -> pd.DataFrame:
"""
Load one redispatch metric file and normalize it to tabular format.
The file is expected to contain one ``<label>: <value>`` line.
:param path: redispatch metric file path
:param redispatch_algorithm: algorithm label override; inferred from the
file name when omitted
:param metric: metric label override (for example ``costs`` or ``volumes``);
inferred from the file name when omitted
:return: validated one-row table with
``redispatch_algorithm``, ``metric``, ``value``
:raises ValueError: if parsing fails or inferred values are invalid
"""
file_path = Path(path)
metric_name = metric or _infer_metric_name(file_path)
algorithm_name = redispatch_algorithm or _infer_redispatch_algorithm_name(file_path)
raw_text = file_path.read_text(encoding="utf-8").strip()
if ":" not in raw_text:
raise ValueError(f"Could not parse redispatch metric file: {file_path}")
_, raw_value = raw_text.split(":", maxsplit=1)
df = pd.DataFrame(
[
{
"redispatch_algorithm": algorithm_name,
"metric": metric_name,
"value": raw_value.strip(),
}
]
)
return validate_redispatch_table(df)
def _infer_metric_name(file_path: Path) -> str:
name = file_path.name
for metric, suffix in REDISPATCH_FILE_PATTERNS.items():
if name.endswith(suffix):
return metric
raise ValueError(f"Could not infer redispatch metric from file name: {file_path.name}")
def _infer_redispatch_algorithm_name(file_path: Path) -> str:
name = file_path.name
for suffix in REDISPATCH_FILE_PATTERNS.values():
if name.endswith(suffix):
return name.removesuffix(suffix).split("_", 1)[0]
raise ValueError(f"Could not infer redispatch algorithm from file name: {file_path.name}")