intermediate backup
This commit is contained in:
544
optim_run.py
Normal file
544
optim_run.py
Normal file
@ -0,0 +1,544 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import yaml
|
||||
import logging
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
from pathlib import Path
|
||||
|
||||
# Import Forecasting Providers
|
||||
from forecasting_model.data_processing import load_raw_data
|
||||
from optimizer.forecasting.base import ForecastProvider
|
||||
from optimizer.forecasting.single_model import SingleModelProvider
|
||||
from optimizer.forecasting.ensemble import EnsembleProvider
|
||||
|
||||
from optimizer.optimization.battery import solve_battery_optimization_hourly
|
||||
from optimizer.utils.optim_config import OptimizationRunConfig
|
||||
from forecasting_model.utils.forecast_config_model import DataConfig, MainConfig
|
||||
|
||||
# Import the newly created loading functions
|
||||
from optimizer.utils.model_io import load_single_model_artifact, load_ensemble_artifact
|
||||
|
||||
from typing import Dict, Any, Optional, Union # Added Union
|
||||
|
||||
# Silence overly verbose libraries if needed
|
||||
mpl_logger = logging.getLogger('matplotlib')
|
||||
mpl_logger.setLevel(logging.WARNING)
|
||||
pil_logger = logging.getLogger('PIL')
|
||||
pil_logger.setLevel(logging.WARNING)
|
||||
|
||||
# --- Basic Logging Setup ---
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)-7s - %(message)s',
|
||||
datefmt='%H:%M:%S')
|
||||
logger = logging.getLogger()
|
||||
|
||||
def load_optimization_config(config_path: str) -> OptimizationRunConfig | None:
|
||||
"""Loads the main optimization configuration from a YAML file."""
|
||||
logger.info(f"Loading optimization config from {config_path}")
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
config_data = yaml.safe_load(f)
|
||||
return OptimizationRunConfig(**config_data)
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Optimization config file not found at {config_path}")
|
||||
return None
|
||||
except yaml.YAMLError as e:
|
||||
logger.error(f"Error parsing YAML optimization config file: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading optimization config: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
def load_main_forecasting_config(config_path: str) -> MainConfig | None:
|
||||
"""Loads the main forecasting configuration from a YAML file."""
|
||||
logger.info(f"Loading main forecasting config from: {config_path}")
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
config_data = yaml.safe_load(f)
|
||||
# Assuming MainConfig is the top-level model in forecast_config_model.py
|
||||
return MainConfig(**config_data)
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Main forecasting config file not found at {config_path}")
|
||||
return None
|
||||
except yaml.YAMLError as e:
|
||||
logger.error(f"Error parsing YAML main forecasting config file: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading main forecasting config: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
# --- Main Execution Logic ---
|
||||
# 1. Load configs
|
||||
# 2. Initialize forecast providers
|
||||
# 3. For each time window:
|
||||
# a. Get forecasts for all horizons
|
||||
# b. Run optimization for each horizon
|
||||
# c. Store results
|
||||
# 4. Evaluate and visualize
|
||||
if __name__ == "__main__":
|
||||
logger.info("Starting battery optimization evaluation with baseline and models/ensembles.")
|
||||
|
||||
# --- Load Main Optimization Config ---
|
||||
optimization_config_path = "optim_config.yaml"
|
||||
optimization_config = load_optimization_config(optimization_config_path)
|
||||
|
||||
if optimization_config is None:
|
||||
logger.critical("Failed to load main optimization config. Exiting.") # Use critical for exit
|
||||
exit(1) # Use non-zero exit code for error
|
||||
|
||||
optim_run_script_dir = Path(__file__).parent
|
||||
|
||||
if not optimization_config.models:
|
||||
logger.critical("No models or ensembles specified in optimization config. Exiting.")
|
||||
exit(1)
|
||||
|
||||
# Try to load the main forecasting config for the first model/ensemble to get the data path
|
||||
first_model_config_path = Path(optimization_config.models[0].model_config_path)
|
||||
main_forecasting_config_for_data = load_main_forecasting_config(str(first_model_config_path))
|
||||
|
||||
if main_forecasting_config_for_data is None:
|
||||
logger.critical("Failed to load forecasting config for the first specified model/ensemble to get data path. Exiting.")
|
||||
exit(1)
|
||||
|
||||
# Use the DataConfig from the first loaded forecasting config
|
||||
historical_data_config = DataConfig(
|
||||
data_path=main_forecasting_config_for_data.data.data_path,
|
||||
raw_datetime_col=main_forecasting_config_for_data.data.raw_datetime_col,
|
||||
raw_datetime_format=main_forecasting_config_for_data.data.raw_datetime_format,
|
||||
datetime_col=main_forecasting_config_for_data.data.datetime_col,
|
||||
raw_target_col=main_forecasting_config_for_data.data.raw_target_col,
|
||||
target_col=main_forecasting_config_for_data.data.target_col,
|
||||
expected_frequency=main_forecasting_config_for_data.data.expected_frequency,
|
||||
fill_initial_target_nans=main_forecasting_config_for_data.data.fill_initial_target_nans
|
||||
)
|
||||
|
||||
logger.info(f"Loading original historical data from: {historical_data_config.data_path}")
|
||||
try:
|
||||
full_historical_df = load_raw_data(historical_data_config)
|
||||
|
||||
if full_historical_df.empty:
|
||||
logger.critical("Loaded original historical data is empty. Cannot proceed. Exiting.")
|
||||
exit(1)
|
||||
|
||||
# Ensure data is at the expected frequency and sorted
|
||||
full_historical_df = full_historical_df.sort_index().asfreq(historical_data_config.expected_frequency)
|
||||
# Fill any NaNs introduced by asfreq if not already handled by fill_initial_target_nans
|
||||
if full_historical_df[historical_data_config.target_col].isnull().any():
|
||||
logger.warning(f"NaNs found after setting frequency {historical_data_config.expected_frequency}. Applying ffill().bfill().")
|
||||
full_historical_df[historical_data_config.target_col] = full_historical_df[historical_data_config.target_col].ffill().bfill()
|
||||
if full_historical_df[historical_data_config.target_col].isnull().any():
|
||||
logger.critical("NaNs still remain after filling. Cannot proceed. Exiting.")
|
||||
exit(1)
|
||||
|
||||
logger.info(f"Original historical data loaded and prepared. Shape: {full_historical_df.shape}")
|
||||
|
||||
except Exception as e:
|
||||
logger.critical(f"Failed to load or prepare original historical data from {historical_data_config.data_path}: {e}", exc_info=True)
|
||||
exit(1)
|
||||
|
||||
|
||||
# --- Define Evaluation Window and Step ---
|
||||
optimization_horizon_hours = optimization_config.optimization_horizon_hours
|
||||
step_size_hours = 1 # Evaluate every hour by sliding the window by 1 hour
|
||||
|
||||
logger.info(f"Using optimization horizon: {optimization_horizon_hours} hours with a step size of {step_size_hours} hour(s).")
|
||||
|
||||
# --- Storage for results per time window ---
|
||||
window_results_list = []
|
||||
|
||||
# --- Load Models/Ensembles and Instantiate Providers ---
|
||||
# Store loaded provider instances, keyed by the name from optim_config
|
||||
forecast_providers: Dict[str, ForecastProvider] = {} # Store provider instances
|
||||
|
||||
for model_eval_config in optimization_config.models:
|
||||
provider_name = model_eval_config.name
|
||||
artifact_type = model_eval_config.type
|
||||
artifact_path = Path(model_eval_config.model_path) # Path to .ckpt or .json
|
||||
config_path = Path(model_eval_config.model_config_path) # Path to YAML config
|
||||
|
||||
provider_instance: Optional[ForecastProvider] = None # Initialize provider instance
|
||||
|
||||
if artifact_type == 'model':
|
||||
logger.info(f"Attempting to load single model artifact and create provider: {provider_name}")
|
||||
target_scaler_path = Path(model_eval_config.target_scaler_path) if model_eval_config.target_scaler_path else None
|
||||
input_size_path = artifact_path.parent / "input_size.pt" # Derive path convention
|
||||
if not input_size_path.exists() and artifact_path.parent.name == 'checkpoints':
|
||||
input_size_path = artifact_path.parent.parent / "input_size.pt"
|
||||
|
||||
loaded_artifact_info = load_single_model_artifact(
|
||||
model_path=artifact_path,
|
||||
config_path=config_path,
|
||||
input_size_path=input_size_path,
|
||||
target_scaler_path=target_scaler_path
|
||||
)
|
||||
|
||||
if loaded_artifact_info:
|
||||
try:
|
||||
provider_instance = SingleModelProvider(
|
||||
model_instance=loaded_artifact_info['model_instance'],
|
||||
feature_config=loaded_artifact_info['feature_config'],
|
||||
target_col=loaded_artifact_info['main_forecasting_config'].data.target_col, # Get target col from loaded config
|
||||
target_scaler=loaded_artifact_info['target_scaler']
|
||||
)
|
||||
# Validation check (basic horizon check)
|
||||
if 1 not in provider_instance.feature_config.forecast_horizon:
|
||||
logger.error(f"Model '{provider_name}' forecast horizon {provider_instance.feature_config.forecast_horizon} does not include 1 hour. Cannot use for this evaluation.")
|
||||
provider_instance = None # Discard if validation fails
|
||||
else:
|
||||
logger.info(f"Successfully created SingleModelProvider for '{provider_name}'.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to instantiate SingleModelProvider for '{provider_name}': {e}", exc_info=True)
|
||||
else:
|
||||
logger.warning(f"Single model artifact '{provider_name}' could not be loaded. Skipping provider creation.")
|
||||
|
||||
elif artifact_type == 'ensemble':
|
||||
logger.info(f"Attempting to load ensemble artifact and create provider: {provider_name}")
|
||||
hpo_base_output_dir_for_ensemble = artifact_path.parent
|
||||
|
||||
loaded_artifact_info = load_ensemble_artifact(
|
||||
ensemble_definition_path=artifact_path,
|
||||
hpo_base_output_dir=hpo_base_output_dir_for_ensemble
|
||||
)
|
||||
|
||||
if loaded_artifact_info:
|
||||
try:
|
||||
# Ensure necessary keys are present before instantiation
|
||||
required_keys = ['fold_artifacts', 'ensemble_method', 'ensemble_feature_config', 'ensemble_target_col']
|
||||
if not all(key in loaded_artifact_info for key in required_keys):
|
||||
missing_keys = [key for key in required_keys if key not in loaded_artifact_info]
|
||||
raise ValueError(f"Ensemble artifact info is missing required keys: {missing_keys}")
|
||||
|
||||
provider_instance = EnsembleProvider(
|
||||
fold_artifacts=loaded_artifact_info['fold_artifacts'],
|
||||
ensemble_method=loaded_artifact_info['ensemble_method'],
|
||||
ensemble_feature_config=loaded_artifact_info['ensemble_feature_config'],
|
||||
ensemble_target_col=loaded_artifact_info['ensemble_target_col']
|
||||
)
|
||||
# Validation check (basic horizon check)
|
||||
if 1 not in provider_instance.common_forecast_horizons:
|
||||
logger.error(f"Ensemble '{provider_name}' common forecast horizon {provider_instance.common_forecast_horizons} does not include 1 hour. Cannot use for this evaluation.")
|
||||
provider_instance = None # Discard if validation fails
|
||||
else:
|
||||
logger.info(f"Successfully created EnsembleProvider for '{provider_name}'.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to instantiate EnsembleProvider for '{provider_name}': {e}", exc_info=True)
|
||||
else:
|
||||
logger.warning(f"Ensemble artifact '{provider_name}' could not be loaded. Skipping provider creation.")
|
||||
|
||||
else:
|
||||
logger.error(f"Unknown artifact type '{artifact_type}' for '{provider_name}'. Skipping.")
|
||||
continue # Skip to next model_eval_config
|
||||
|
||||
# Store the successfully created provider instance
|
||||
if provider_instance:
|
||||
forecast_providers[provider_name] = provider_instance
|
||||
|
||||
# --- End Loading ---
|
||||
|
||||
|
||||
if not forecast_providers:
|
||||
logger.critical("No forecast providers were successfully created. Cannot proceed with evaluation. Exiting.")
|
||||
exit(1)
|
||||
|
||||
# --- Calculate Max Lookback Needed Across All Providers ---
|
||||
max_required_lookback = 0
|
||||
for provider_name, provider in forecast_providers.items():
|
||||
try:
|
||||
lookback = provider.get_required_lookback()
|
||||
max_required_lookback = max(max_required_lookback, lookback)
|
||||
logger.debug(f"Provider '{provider_name}' requires lookback: {lookback}")
|
||||
except AttributeError:
|
||||
logger.error(f"Provider '{provider_name}' does not have a 'get_required_lookback' method. Cannot determine lookback requirements accurately. Exiting.")
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting lookback for provider '{provider_name}': {e}. Exiting.", exc_info=True)
|
||||
exit(1)
|
||||
|
||||
logger.info(f"Maximum lookback required across all providers: {max_required_lookback} hours.")
|
||||
|
||||
# The first timestamp for which we can generate a forecast needs `max_required_lookback` points *before* it.
|
||||
# If optimization starts at `window_start_time` (iloc `i`), the forecast generation needs data up to `i-1`.
|
||||
# The historical slice passed to `get_forecast` must contain `max_required_lookback` points, ending at `i-1`.
|
||||
# Therefore, the slice starts at `i - max_required_lookback`. This must be >= 0.
|
||||
# So, `i >= max_required_lookback`.
|
||||
first_window_start_iloc = max_required_lookback
|
||||
|
||||
# The last window starts such that the window ends within the data: `i + optimization_horizon_hours - 1 < len(df)`
|
||||
# So, `i < len(df) - optimization_horizon_hours + 1`.
|
||||
last_window_start_iloc = len(full_historical_df) - optimization_horizon_hours
|
||||
|
||||
if first_window_start_iloc > last_window_start_iloc:
|
||||
logger.critical(f"Not enough historical data ({len(full_historical_df)} hours) for the required lookback ({max_required_lookback}) and optimization horizon ({optimization_horizon_hours}). First possible window start iloc: {first_window_start_iloc}, last possible: {last_window_start_iloc}. Exiting.")
|
||||
exit(1)
|
||||
|
||||
logger.info(f"Evaluating over historical windows from iloc {first_window_start_iloc} to {last_window_start_iloc}.")
|
||||
|
||||
# --- Evaluation Loop ---
|
||||
for i in range(first_window_start_iloc, last_window_start_iloc + 1, step_size_hours):
|
||||
# Define the actual optimization window in terms of iloc and time
|
||||
window_start_iloc = i
|
||||
window_end_iloc = i + optimization_horizon_hours - 1 # Inclusive index for the window end
|
||||
|
||||
# Check if the window is complete within the dataset bounds
|
||||
if window_end_iloc >= len(full_historical_df):
|
||||
logger.warning(f"Skipping window starting at iloc {window_start_iloc}: extends beyond available data (needs up to iloc {window_end_iloc}, max is {len(full_historical_df)-1}).")
|
||||
continue
|
||||
|
||||
window_timestamps = full_historical_df.index[window_start_iloc : window_end_iloc + 1]
|
||||
|
||||
# Double-check length just in case
|
||||
if len(window_timestamps) != optimization_horizon_hours:
|
||||
logger.warning(f"Skipping window starting at iloc {window_start_iloc} due to unexpected timestamp slice length ({len(window_timestamps)} instead of {optimization_horizon_hours} hours).")
|
||||
continue
|
||||
|
||||
window_start_time = window_timestamps[0]
|
||||
window_end_time = window_timestamps[-1]
|
||||
logger.info(f"Processing window: {window_start_time.strftime('%Y-%m-%d %H:%M')} to {window_end_time.strftime('%Y-%m-%d %H:%M')} (iloc {window_start_iloc})")
|
||||
|
||||
# --- Prepare Historical Slice for Forecasting ---
|
||||
# We need data *up to* the beginning of the optimization window, including lookback.
|
||||
# Slice should end at iloc `window_start_iloc - 1`.
|
||||
# Slice should start at `window_start_iloc - max_required_lookback`.
|
||||
hist_slice_start_iloc = max(0, window_start_iloc - max_required_lookback)
|
||||
hist_slice_end_iloc = window_start_iloc # Exclusive end iloc for slicing, so it includes up to window_start_iloc - 1
|
||||
|
||||
if hist_slice_end_iloc <= hist_slice_start_iloc:
|
||||
logger.error(f"Invalid historical slice range for window starting at {window_start_time}: start_iloc={hist_slice_start_iloc}, end_iloc={hist_slice_end_iloc}. Skipping window.")
|
||||
continue
|
||||
|
||||
historical_slice_for_forecasting = full_historical_df.iloc[hist_slice_start_iloc : hist_slice_end_iloc].copy()
|
||||
|
||||
# Check if the slice has the expected length (at least max_required_lookback, unless near start of data)
|
||||
if len(historical_slice_for_forecasting) < max_required_lookback and window_start_iloc >= max_required_lookback:
|
||||
logger.warning(f"Historical slice for window starting {window_start_time} has unexpected length {len(historical_slice_for_forecasting)}, expected {max_required_lookback}. Check slicing logic. Skipping.")
|
||||
continue
|
||||
elif len(historical_slice_for_forecasting) == 0:
|
||||
logger.warning(f"Historical slice for window starting {window_start_time} is empty. Skipping.")
|
||||
continue
|
||||
|
||||
logger.debug(f"Using historical slice from {historical_slice_for_forecasting.index.min()} to {historical_slice_for_forecasting.index.max()} (Length: {len(historical_slice_for_forecasting)}) for forecasting.")
|
||||
|
||||
|
||||
# --- Collect Window Results ---
|
||||
window_results = {
|
||||
'start_time': window_start_time,
|
||||
'end_time': window_end_time,
|
||||
'actual_prices': full_historical_df[historical_data_config.target_col].iloc[window_start_iloc : window_end_iloc + 1].tolist()
|
||||
}
|
||||
|
||||
# --- Baseline Optimization ---
|
||||
baseline_prices_input = np.array(window_results['actual_prices'])
|
||||
logger.debug(f"Running baseline optimization for window starting {window_start_time}")
|
||||
try:
|
||||
baseline_status, baseline_profit, baseline_power, baseline_B = solve_battery_optimization_hourly(
|
||||
baseline_prices_input,
|
||||
optimization_config.initial_b,
|
||||
optimization_config.max_capacity,
|
||||
optimization_config.max_rate
|
||||
)
|
||||
window_results['baseline'] = {
|
||||
"status": baseline_status,
|
||||
"profit": baseline_profit,
|
||||
"power_schedule": baseline_power.tolist() if baseline_power is not None else None,
|
||||
"B_schedule": baseline_B.tolist() if baseline_B is not None else None
|
||||
}
|
||||
logger.debug(f"Baseline profit: {baseline_profit if baseline_profit is not None else 'N/A'}")
|
||||
except Exception as e:
|
||||
logger.error(f"Baseline optimization failed for window starting {window_start_time}: {e}", exc_info=True)
|
||||
window_results['baseline'] = {"status": "Error", "profit": None, "power_schedule": None, "B_schedule": None}
|
||||
|
||||
|
||||
# --- Forecast Provider Optimizations ---
|
||||
for provider_name, provider_instance in forecast_providers.items():
|
||||
logger.debug(f"Generating forecast and running optimization for provider '{provider_name}' for window starting {window_start_time}")
|
||||
|
||||
# Generate forecast using the provider's get_forecast method
|
||||
try:
|
||||
forecast_prices_input = provider_instance.get_forecast(
|
||||
historical_data_slice=historical_slice_for_forecasting.copy(), # Pass a copy
|
||||
optimization_horizon_hours=optimization_horizon_hours
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error calling get_forecast for provider '{provider_name}': {e}", exc_info=True)
|
||||
forecast_prices_input = None
|
||||
|
||||
|
||||
if forecast_prices_input is None or len(forecast_prices_input) != optimization_horizon_hours:
|
||||
logger.warning(f"Forecast generation failed or returned incorrect length ({len(forecast_prices_input) if forecast_prices_input is not None else 0} instead of {optimization_horizon_hours}) for provider '{provider_name}' window starting {window_start_time}. Skipping optimization.")
|
||||
window_results[provider_name] = {"status": "Forecast Generation Failed", "profit": None, "power_schedule": None, "B_schedule": None}
|
||||
continue # Skip optimization for this provider/window
|
||||
|
||||
# Ensure the forecast input is a numpy array of the correct shape
|
||||
if not isinstance(forecast_prices_input, np.ndarray) or forecast_prices_input.shape != (optimization_horizon_hours,):
|
||||
logger.error(f"Forecast input for provider '{provider_name}' has incorrect format ({type(forecast_prices_input)}, shape {forecast_prices_input.shape if isinstance(forecast_prices_input, np.ndarray) else 'N/A'}). Expected ({optimization_horizon_hours},). Skipping optimization.")
|
||||
window_results[provider_name] = {"status": "Invalid Forecast Format", "profit": None, "power_schedule": None, "B_schedule": None}
|
||||
continue
|
||||
|
||||
# --- Run Optimization with Forecast Prices ---
|
||||
try:
|
||||
model_status, model_profit, model_power, model_B = solve_battery_optimization_hourly(
|
||||
forecast_prices_input,
|
||||
optimization_config.initial_b,
|
||||
optimization_config.max_capacity,
|
||||
optimization_config.max_rate
|
||||
)
|
||||
|
||||
window_results[provider_name] = {
|
||||
"status": model_status,
|
||||
"profit": model_profit,
|
||||
"power_schedule": model_power.tolist() if model_power is not None else None,
|
||||
"B_schedule": model_B.tolist() if model_B is not None else None
|
||||
}
|
||||
logger.debug(f"Provider '{provider_name}' profit: {model_profit if model_profit is not None else 'N/A'}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Optimization failed for provider '{provider_name}' window starting {window_start_time}: {e}", exc_info=True)
|
||||
window_results[provider_name] = {"status": "Error", "profit": None, "power_schedule": None, "B_schedule": None}
|
||||
|
||||
# Append results for this window
|
||||
window_results_list.append(window_results)
|
||||
logger.debug(f"Finished processing window starting at: {window_start_time.strftime('%Y-%m-%d %H:%M')}")
|
||||
|
||||
logger.info("Finished processing all evaluation windows.")
|
||||
|
||||
|
||||
# --- Post-processing and Plotting ---
|
||||
logger.info("Starting results analysis and plotting.")
|
||||
|
||||
if not window_results_list:
|
||||
logger.warning("No window results were collected. Skipping plotting.")
|
||||
exit(0) # Not necessarily an error state
|
||||
|
||||
# Convert results list to a DataFrame
|
||||
flat_results = []
|
||||
successfully_loaded_provider_names = list(forecast_providers.keys()) # Names of providers used
|
||||
|
||||
for window_res in window_results_list:
|
||||
base_info = {
|
||||
'start_time': window_res['start_time'],
|
||||
'end_time': window_res['end_time'],
|
||||
}
|
||||
# Add baseline results
|
||||
flat_results.append({**base_info, 'type': 'baseline', **window_res.get('baseline', {})})
|
||||
# Add provider results
|
||||
for provider_name in successfully_loaded_provider_names:
|
||||
provider_res = window_res.get(provider_name, {}) # Get results or empty dict
|
||||
flat_results.append({**base_info, 'type': provider_name, **provider_res})
|
||||
|
||||
|
||||
results_df = pd.DataFrame(flat_results)
|
||||
results_df['start_time'] = pd.to_datetime(results_df['start_time']) # Ensure datetime type
|
||||
|
||||
# Filter out rows where essential optimization results are missing
|
||||
# results_df.dropna(subset=['profit', 'power_schedule'], inplace=True) # Be careful with dropna
|
||||
|
||||
# Calculate Profit Absolute Error over time
|
||||
profit_pivot = results_df.pivot_table(index='start_time', columns='type', values='profit')
|
||||
|
||||
mae_df = pd.DataFrame(index=profit_pivot.index)
|
||||
if 'baseline' in profit_pivot.columns:
|
||||
for provider_name in successfully_loaded_provider_names:
|
||||
if provider_name in profit_pivot.columns:
|
||||
# Use .sub() and .abs() to handle potential NaNs gracefully
|
||||
mae_df[f'Profit_Abs_Error_{provider_name}'] = profit_pivot[provider_name].sub(profit_pivot['baseline']).abs()
|
||||
else:
|
||||
logger.warning(f"Cannot calculate profit MAE for provider '{provider_name}'. Data not found in pivoted results.")
|
||||
else:
|
||||
logger.warning("Cannot calculate profit MAE because baseline results are missing or incomplete.")
|
||||
|
||||
|
||||
# --- Plotting ---
|
||||
|
||||
# Plot 1: Price and First Hour's Power Schedule Over Time
|
||||
logger.info("Generating Price and Power Schedule plot.")
|
||||
|
||||
continuous_power_data = []
|
||||
for window_res in window_results_list:
|
||||
start_time = window_res['start_time']
|
||||
# Baseline power
|
||||
baseline_data = window_res.get('baseline', {})
|
||||
if baseline_data.get('power_schedule') and len(baseline_data['power_schedule']) > 0:
|
||||
continuous_power_data.append({'time': start_time, 'type': 'baseline', 'power': baseline_data['power_schedule'][0]})
|
||||
# Provider powers
|
||||
for provider_name in successfully_loaded_provider_names:
|
||||
provider_data = window_res.get(provider_name, {})
|
||||
if provider_data.get('power_schedule') and len(provider_data['power_schedule']) > 0:
|
||||
continuous_power_data.append({'time': start_time, 'type': provider_name, 'power': provider_data['power_schedule'][0]})
|
||||
|
||||
continuous_power_df = pd.DataFrame(continuous_power_data)
|
||||
if not continuous_power_df.empty:
|
||||
continuous_power_df['time'] = pd.to_datetime(continuous_power_df['time'])
|
||||
|
||||
# Get historical prices corresponding to the evaluation window start times
|
||||
eval_start_times = results_df['start_time'].unique()
|
||||
price_plot_df = full_historical_df.loc[eval_start_times, [historical_data_config.target_col]].reset_index()
|
||||
price_plot_df.rename(columns={price_plot_df.columns[0]: 'time', historical_data_config.target_col: 'price'}, inplace=True) # Use positional index for timestamp column rename
|
||||
|
||||
plot_range_start = continuous_power_df['time'].min()
|
||||
plot_range_end = continuous_power_df['time'].max()
|
||||
|
||||
# Filter data for the plot range
|
||||
filtered_price_df = price_plot_df[(price_plot_df['time'] >= plot_range_start) & (price_plot_df['time'] <= plot_range_end)]
|
||||
filtered_power_df = continuous_power_df[(continuous_power_df['time'] >= plot_range_start) & (continuous_power_df['time'] <= plot_range_end)]
|
||||
|
||||
if not filtered_power_df.empty:
|
||||
fig1, ax1 = plt.subplots(figsize=(15, 7))
|
||||
ax2 = ax1.twinx()
|
||||
|
||||
sns.lineplot(data=filtered_price_df, x='time', y='price', ax=ax1, color='gray', linestyle='--', label='Historical Price (Window Start)', zorder=1)
|
||||
ax1.set_ylabel('Price (€/MWh)', color='gray')
|
||||
ax1.tick_params(axis='y', labelcolor='gray')
|
||||
|
||||
sns.lineplot(data=filtered_power_df, x='time', y='power', hue='type', ax=ax2, zorder=2)
|
||||
ax2.set_ylabel('Power (MW)')
|
||||
|
||||
h1, l1 = ax1.get_legend_handles_labels()
|
||||
h2, l2 = ax2.get_legend_handles_labels()
|
||||
ax2.legend(h1 + h2, l1 + l2, loc='upper left', title='Schedule Type')
|
||||
ax1.get_legend().remove() # Remove the original legend from ax1
|
||||
|
||||
ax1.set_xlabel('Time')
|
||||
ax1.set_title('Battery Power Schedule (1st Hour) vs. Historical Price (Window Start)')
|
||||
plt.tight_layout()
|
||||
plt.savefig("power_schedule_vs_price.png")
|
||||
logger.info("Price and Power Schedule plot saved as power_schedule_vs_price.png")
|
||||
# plt.show()
|
||||
else:
|
||||
logger.warning("No power data available within the determined plot range.")
|
||||
|
||||
else:
|
||||
logger.warning("No continuous power data generated for plotting power schedule.")
|
||||
|
||||
# Plot 2: Absolute Profit Error over time
|
||||
logger.info("Generating Profit Absolute Error plot.")
|
||||
if not mae_df.empty and not mae_df.isnull().all().all(): # Check if not empty and not all NaN
|
||||
fig2, ax = plt.subplots(figsize=(15, 7))
|
||||
|
||||
# Use the plot range from the power plot if available
|
||||
mae_plot_range_start = plot_range_start if 'plot_range_start' in locals() else mae_df.index.min()
|
||||
mae_plot_range_end = plot_range_end if 'plot_range_end' in locals() else mae_df.index.max()
|
||||
|
||||
filtered_mae_df = mae_df[(mae_df.index >= mae_plot_range_start) & (mae_df.index <= mae_plot_range_end)].copy() # Create copy
|
||||
# Optional: Handle or remove columns that are all NaN within the range
|
||||
filtered_mae_df.dropna(axis=1, how='all', inplace=True)
|
||||
|
||||
if not filtered_mae_df.empty:
|
||||
sns.lineplot(data=filtered_mae_df, ax=ax)
|
||||
ax.set_xlabel('Time')
|
||||
ax.set_ylabel('Absolute Profit Error vs. Baseline (€)')
|
||||
ax.set_title('Absolute Profit Error of Providers vs. Baseline over Time')
|
||||
ax.legend(title='Provider Type')
|
||||
plt.tight_layout()
|
||||
plt.savefig("profit_abs_error_over_time.png")
|
||||
logger.info("Profit Absolute Error plot saved as profit_abs_error_over_time.png")
|
||||
# plt.show()
|
||||
else:
|
||||
logger.warning("MAE data is all NaN or empty within the plot range. Skipping MAE plot.")
|
||||
else:
|
||||
logger.warning("No valid data available to plot Profit Absolute Error.")
|
||||
|
||||
logger.info("Evaluation and plotting completed.")
|
Reference in New Issue
Block a user