Ask me anything
Ithy Logo
Sign in to save

Unlock Future Power Output: Simulating Rankine-Hirn Generation with XGBoost

A step-by-step guide with Python code to forecast minute-by-minute energy production for the next 7 days.

xgboost-rankine-hirn-power-forecast-44yd101l

Predicting the power generation of complex systems like Rankine-Hirn cycle installations is crucial for efficient energy management and grid stability. This guide provides a realistic simulation using the powerful XGBoost algorithm to forecast power output minute-by-minute for the upcoming week (10,080 minutes). The forecast relies solely on historical generation data, extracting patterns from time-based features and past output values (lags).

Key Highlights of the Simulation

  • Realistic Forecasting: Simulates minute-by-minute power output using a recursive approach, where each prediction informs the next.
  • XGBoost Power: Leverages the XGBoost algorithm, known for its accuracy and ability to capture complex, non-linear relationships in time series data.
  • Focused Inputs: Utilizes only historical power data, time-based features (like hour and day), and lagged outputs, demonstrating what's possible with limited input variables.

Understanding the Rankine-Hirn Cycle & Forecasting Challenge

The Thermodynamics of Power Generation

The Rankine cycle, often enhanced with superheating in the Rankine-Hirn variant, forms the backbone of many steam-based power generation systems, including those utilizing waste heat, geothermal energy, or traditional fuel sources. It involves a working fluid (typically water/steam) passing through four main stages: pumping, boiling (in a boiler), expansion (through a turbine to generate power), and condensation (in a condenser). Understanding these stages helps appreciate the factors influencing power output, although our model will focus purely on the historical time series patterns.

Turbine at a Power Station

Steam turbines are critical components in Rankine cycle power generation.

Why Time Series Forecasting with XGBoost?

Forecasting energy generation minute-by-minute over a long horizon (7 days) is challenging. Power output often exhibits complex patterns related to time of day, day of week, and inherent operational cycles, alongside random fluctuations. XGBoost (Extreme Gradient Boosting) is well-suited because:

  • It builds multiple decision trees sequentially, with each tree correcting the errors of the previous one.
  • It effectively handles numerical features like time components and lagged values.
  • It can capture intricate interactions between these features without assuming linear relationships.

However, standard XGBoost doesn't inherently understand the temporal order of data. We address this by transforming the time series problem into a supervised learning problem using lagged values as features and employing a recursive strategy for multi-step forecasting.

The Recursive Forecasting Approach

To predict 10,080 minutes ahead, we can't just train a model and predict all steps at once. Instead, we use a recursive method:

  1. Train the model to predict the *next* minute's power based on past lags and current time features.
  2. Predict minute 1.
  3. Use the predicted value for minute 1 as part of the input (as a lag) to predict minute 2.
  4. Continue this process, using the latest prediction to generate inputs for the subsequent prediction, for all 10,080 steps.

A key challenge here is potential error accumulation – inaccuracies in early predictions can propagate through later forecasts.


Python Simulation: Step-by-Step Implementation

Setting Up the Environment

This code is designed for a Python environment, ideally within a Jupyter Notebook. You'll need libraries like `pandas`, `numpy`, `xgboost`, `scikit-learn`, and `matplotlib`.


# Import necessary libraries
import numpy as np
import pandas as pd
import xgboost as xgb
from sklearn.model_selection import train_test_split # For splitting data
from sklearn.metrics import mean_squared_error, mean_absolute_error # For evaluation
import matplotlib.pyplot as plt
from datetime import datetime, timedelta

# For reproducibility
np.random.seed(42)
plt.style.use('seaborn-v0_8-whitegrid') # Using a nice plot style
    

Step 1: Generating Synthetic Historical Data

Since real minute-by-minute Rankine-Hirn cycle data isn't provided, we'll generate synthetic data that mimics typical power generation patterns. This includes a base load, daily cyclical variations (sinusoidal), and some random noise.


# --- Data Generation ---
print("Generating synthetic historical data...")
# Simulate 60 days of minute-level data for training/validation
simulation_duration_days = 60
total_minutes_history = simulation_duration_days * 24 * 60

# Generate timestamps ending 'yesterday' relative to the knowledge cutoff
# Knowledge cutoff: 2025-05-03. Let's generate data up to 2025-05-02 23:59
end_history_date = datetime(2025, 5, 2, 23, 59)
start_history_date = end_history_date - timedelta(minutes=total_minutes_history - 1)
date_rng = pd.date_range(start=start_history_date, periods=total_minutes_history, freq='T') # 'T' for minute frequency

# Simulate power output (e.g., in Megawatts - MW)
# Base load + daily cycle (higher during day) + weekly cycle (lower weekends) + noise
base_power = 50 # MW
daily_amplitude = 15 # MW variation daily
weekly_amplitude = 5 # MW variation weekly
noise_level = 3 # MW standard deviation

# Calculate time components for simulation
minutes_in_day = 24 * 60
minutes_in_week = 7 * minutes_in_day
day_progress = (date_rng.hour * 60 + date_rng.minute) / minutes_in_day
week_progress = ((date_rng.dayofweek * minutes_in_day) + (date_rng.hour * 60 + date_rng.minute)) / minutes_in_week

# Combine components
daily_cycle = daily_amplitude * np.sin(2 * np.pi * day_progress)
weekly_cycle = -weekly_amplitude * np.sin(np.pi * date_rng.dayofweek / 6) # Simple weekend dip approx
random_noise = np.random.normal(0, noise_level, size=total_minutes_history)

# Combine to get power output
power_output = base_power + daily_cycle + weekly_cycle + random_noise
# Ensure power doesn't go below a minimum threshold (e.g., 5 MW)
power_output = np.maximum(5, power_output)

# Create DataFrame
df_history = pd.DataFrame({'timestamp': date_rng, 'power_mw': power_output})
df_history.set_index('timestamp', inplace=True)

print(f"Generated {len(df_history)} minutes of historical data.")
print(df_history.head())
print(df_history.tail())

# Visualize a sample of the generated data
plt.figure(figsize=(15, 5))
plt.plot(df_history.index[-2*1440:], df_history['power_mw'][-2*1440:]) # Plot last 2 days
plt.title('Sample of Generated Historical Power Data (Last 48 Hours)')
plt.xlabel('Timestamp')
plt.ylabel('Power Output (MW)')
plt.show()
    

Step 2: Feature Engineering

Now, we transform the raw time series data into a format suitable for XGBoost. This involves extracting time-based features and creating lagged features.


# --- Feature Engineering ---
def create_features(df, target_col='power_mw'):
    """Creates time-based and lag features for the dataframe."""
    df_feat = df.copy()

    # Time-based features
    df_feat['minute'] = df_feat.index.minute
    df_feat['hour'] = df_feat.index.hour
    df_feat['dayofweek'] = df_feat.index.dayofweek # Monday=0, Sunday=6
    df_feat['dayofmonth'] = df_feat.index.day
    df_feat['month'] = df_feat.index.month
    df_feat['dayofyear'] = df_feat.index.dayofyear

    # Lag features (using past power values)
    # Choose lags that might capture short-term dynamics and daily patterns
    lag_minutes = [1, 5, 15, 30, 60, 120, 1440] # 1m, 5m, 15m, 30m, 1h, 2h, 1 day ago
    for lag in lag_minutes:
        df_feat[f'lag_{lag}_min'] = df_feat[target_col].shift(lag)

    # Drop rows with NaN values created by lagging (at the beginning of the series)
    df_feat.dropna(inplace=True)
    return df_feat

print("Engineering features...")
df_features = create_features(df_history, target_col='power_mw')
print(f"Created features. Shape of feature DataFrame: {df_features.shape}")
print(df_features.head())
    

Step 3: Data Splitting (Train/Validation)

It's crucial to split time series data chronologically to avoid lookahead bias. We'll train on the earlier part and validate/test on the later part.


# --- Train/Validation Split ---
# Split data chronologically. Let's use the last 7 days for validation.
validation_duration_minutes = 7 * 24 * 60
split_index = len(df_features) - validation_duration_minutes

train_df = df_features.iloc[:split_index]
val_df = df_features.iloc[split_index:]

# Define features (X) and target (y)
target_column = 'power_mw'
feature_columns = [col for col in df_features.columns if col != target_column]

X_train, y_train = train_df[feature_columns], train_df[target_column]
X_val, y_val = val_df[feature_columns], val_df[target_column]

print(f"Training data shape: X={X_train.shape}, y={y_train.shape}")
print(f"Validation data shape: X={X_val.shape}, y={y_val.shape}")
    

Step 4: Training the XGBoost Model

We initialize and train the XGBoost regressor. Using early stopping helps prevent overfitting by monitoring performance on the validation set.


# --- Model Training ---
print("Training XGBoost model...")
xgb_model = xgb.XGBRegressor(
    objective='reg:squarederror', # Objective function for regression
    n_estimators=1000,            # Max number of boosting rounds (trees)
    learning_rate=0.05,           # Step size shrinkage
    max_depth=7,                  # Max depth of individual trees
    subsample=0.8,                # Fraction of samples used per tree
    colsample_bytree=0.8,         # Fraction of features used per tree
    random_state=42,              # Reproducibility
    n_jobs=-1,                    # Use all available CPU cores
    early_stopping_rounds=50      # Stop if validation metric doesn't improve for 50 rounds
)

# Train the model
xgb_model.fit(
    X_train, y_train,
    eval_set=[(X_train, y_train), (X_val, y_val)], # Evaluate on training and validation sets
    verbose=100 # Print evaluation metrics every 100 rounds
)

print("Model training complete.")

# Evaluate on validation set (one-step ahead predictions)
y_pred_val = xgb_model.predict(X_val)
rmse_val = np.sqrt(mean_squared_error(y_val, y_pred_val))
mae_val = mean_absolute_error(y_val, y_pred_val)
print(f"Validation Set Performance (One-Step Ahead):")
print(f"  RMSE: {rmse_val:.3f} MW")
print(f"  MAE:  {mae_val:.3f} MW")
    

Step 5: Recursive Forecasting for the Next 7 Days

This is the core of the simulation. We predict minute-by-minute for 10,080 steps, using the model's output from the previous step to generate the lag features for the current step.


# --- Recursive Forecasting ---
print("Starting recursive forecast for the next 7 days (10,080 minutes)...")

# Forecast horizon
forecast_horizon_minutes = 7 * 24 * 60

# Get the last known timestamp from our historical data
last_timestamp = df_history.index[-1]

# Create future timestamps
future_timestamps = pd.date_range(
    start=last_timestamp + timedelta(minutes=1),
    periods=forecast_horizon_minutes,
    freq='T'
)

# Prepare the initial input data for forecasting
# We need the last row of features used for training/validation, which corresponds to the last actual data point
# However, the 'create_features' function shifted data, so the last row of `df_features` aligns with the last row of `df_history`
# We need the actual power values from the history to seed the lag features
history_for_lags = df_history[target_column].values # Get all historical power values as numpy array

# Initialize list to store forecast results
forecasted_power = []
# List to store the features used for each prediction step (for debugging/analysis if needed)
# forecast_features_history = []

# Get the lag values needed for the *first* prediction step
# These come from the end of the actual historical data
current_lags = {}
lag_minutes_list = [1, 5, 15, 30, 60, 120, 1440] # Must match lags used in create_features
for lag in lag_minutes_list:
     # Access historical data correctly: history_for_lags[-lag] gets the value 'lag' minutes before the end
    current_lags[f'lag_{lag}_min'] = history_for_lags[-lag]


# Recursive loop
for i in range(forecast_horizon_minutes):
    current_timestamp = future_timestamps[i]

    # 1. Create time features for the current timestamp
    time_features = {
        'minute': current_timestamp.minute,
        'hour': current_timestamp.hour,
        'dayofweek': current_timestamp.dayofweek,
        'dayofmonth': current_timestamp.day,
        'month': current_timestamp.month,
        'dayofyear': current_timestamp.dayofyear
    }

    # 2. Combine time features and the current lag features
    current_features_dict = {**time_features, **current_lags}

    # Ensure feature order matches the model's training columns
    current_features_df = pd.DataFrame([current_features_dict], columns=feature_columns)

    # Store features used (optional)
    # forecast_features_history.append(current_features_df.iloc[0].to_dict())

    # 3. Make the prediction for the current timestamp
    predicted_value = xgb_model.predict(current_features_df)[0]
    # Ensure prediction is non-negative (optional, based on domain knowledge)
    predicted_value = max(0, predicted_value)

    # 4. Store the prediction
    forecasted_power.append(predicted_value)

    # 5. Update the lag features for the *next* iteration
    # The newest prediction becomes lag_1 for the next step
    # Other lags shift: previous lag_1 becomes lag_5 input if relevant, etc.
    # We need to be careful here. We update the `current_lags` dictionary.
    new_lags = {}
    new_lags['lag_1_min'] = predicted_value # The prediction just made is the 1-minute lag for the *next* step

    # Update other lags based on previous values stored in `current_lags` and the new prediction
    # This requires keeping a short history of recent predictions or cleverly updating the dict
    # Simple approach: Shift existing lags and insert the new prediction
    # Note: This assumes lag intervals are simple shifts. More complex dependencies might require storing recent predictions.
    for lag in lag_minutes_list:
        if lag != 1:
             # The value that was lag `l-1` (if `l-1` exists in our lag list) is now approximately lag `l`
             # This is an approximation. A more robust way is to maintain a short buffer of recent predictions.
             # Let's refine: We need the value from `lag-1` minutes ago.
             # If lag=5, we need the value from 4 mins ago relative to the *next* step, which is value from 3 mins ago relative to *current* step.
             # This gets complex quickly. Let's use the simpler shift for this example:
             # The value that *was* lag X becomes lag Y in the next step.
             # e.g., value at lag_1 becomes lag_5 input eventually, value at lag_5 becomes lag_15...
             # This is tricky. Let's rethink:
             # We need the power value from 'lag' minutes *before* the timestamp we are *about to predict*.
             # Let `t` be the current timestamp (index `i` in the loop, `future_timestamps[i]`). We are predicting for `t`.
             # We need features for time `t`. Lags are values at `t-lag`.
             # For the *next* step (`t+1`), we need lags from `(t+1)-lag`.
             # `lag_1` for step `t+1` is the value predicted at step `t`. (Handled by `new_lags['lag_1_min'] = predicted_value`)
             # `lag_5` for step `t+1` is the value from `(t+1)-5 = t-4`. This was the prediction from 4 steps ago.
             # `lag_15` for step `t+1` is the value from `t-14`. Prediction from 14 steps ago.

             # We need to store recent predictions to update lags correctly.
             # Store the last `max_lag` predictions. `max_lag = 1440` here.

             # Let's store predictions temporarily and update lags from this buffer.
             if i < max(lag_minutes_list):
                 # Need to fetch from historical data + previous predictions
                 lookback_time = current_timestamp - timedelta(minutes=lag-1) # Time t - (lag-1) corresponds to lag L for step t+1
                 if lookback_time in df_history.index:
                     new_lags[f'lag_{lag}_min'] = df_history.loc[lookback_time, target_column]
                 elif (i - (lag-1)) >= 0: # Check if the index exists in forecasted_power
                     new_lags[f'lag_{lag}_min'] = forecasted_power[i - (lag-1)]
                 else:
                     new_lags[f'lag_{lag}_min'] = np.nan # Or handle appropriately (e.g., mean fill)
             else:
                 # Fetch from the predictions list
                  new_lags[f'lag_{lag}_min'] = forecasted_power[i - (lag-1)] # Value predicted `lag-1` steps ago


    # Update the current_lags dictionary for the next loop iteration
    # Handle potential NaNs if history wasn't long enough (though dropna should prevent this if history > max_lag)
    for key, val in new_lags.items():
        if pd.isna(val):
             # Simple fill with the last known lag value or overall mean if necessary
             new_lags[key] = current_lags.get(key, np.mean(history_for_lags)) # Fallback

    current_lags = new_lags

print("Recursive forecast complete.")

# Create a DataFrame for the forecast results
forecast_df = pd.DataFrame({
    'timestamp': future_timestamps,
    'predicted_power_mw': forecasted_power
})
forecast_df.set_index('timestamp', inplace=True)

print(forecast_df.head())
print(forecast_df.tail())

    

Note: The recursive forecasting logic, especially updating lags correctly, can be complex. The implementation above uses recent predictions to update future lag features. Careful testing and potentially more sophisticated state management might be needed for production systems.

Step 6: Visualizing the Forecast

Plotting the forecast alongside recent historical data helps visualize the model's predictions.


# --- Visualization ---
plt.figure(figsize=(18, 7))

# Plot recent historical data (e.g., last 2 days)
plt.plot(df_history.index[-2*1440:], df_history['power_mw'][-2*1440:], label='Historical Power (Last 48h)', color='blue')

# Plot the forecasted data
plt.plot(forecast_df.index, forecast_df['predicted_power_mw'], label='Forecasted Power (Next 7 Days)', color='red', linestyle='--')

plt.title('Rankine-Hirn Cycle Power Output: Historical Data & 7-Day Forecast')
plt.xlabel('Timestamp')
plt.ylabel('Power Output (MW)')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

# Plot feature importance
plt.figure(figsize=(10, 8))
xgb.plot_importance(xgb_model, height=0.8)
plt.title('XGBoost Feature Importance')
plt.tight_layout()
plt.show()
    

Understanding Model Inputs and Performance

Key Feature Contributions

The feature importance plot generated by XGBoost reveals which inputs the model relied on most heavily for its predictions. Typically, recent lags (like `lag_1_min`) and key time features (like `hour` or `dayofweek`) are highly influential in energy forecasting. This radar chart provides a conceptual view of the relative importance of different feature types and model characteristics for this kind of task.

This chart suggests that recent power outputs (lags) and immediate time features (hour) are often critical. While XGBoost is fast and handles noise well, capturing long-term trends solely from lags can be a limitation (as noted in Answer A's reference to Stack Exchange). Adding external factors like weather or operational status could significantly boost performance.


Visualizing the Forecasting Workflow

From Data to Prediction

The entire process, from gathering data to generating the final forecast, follows a structured workflow. This mindmap illustrates the key stages involved in building this XGBoost time series forecasting simulation.

mindmap root["Rankine-Hirn Power Forecasting
using XGBoost"] id1["1. Data Preparation"] id1a["Acquire Historical Data
(Minute-by-Minute Power MW)"] id1b["Generate Synthetic Data
(if real data unavailable)"] id1c["Clean & Preprocess Data
(Handle missing values - if any)"] id1d["Set Time Index"] id2["2. Feature Engineering"] id2a["Extract Time-Based Features
(Minute, Hour, DayOfWeek, DayOfYear, etc.)"] id2b["Create Lagged Features
(Power at t-1, t-5, t-60, t-1440 mins, etc.)"] id2c["(Optional) Rolling Statistics
(Mean/Std over windows)"] id2d["Handle NaNs from Lags"] id3["3. Model Training"] id3a["Split Data Chronologically
(Train / Validation Sets)"] id3b["Select Features (X) and Target (y)"] id3c["Initialize XGBoost Regressor
(Set Hyperparameters)"] id3d["Train Model with Early Stopping
(Monitor Validation Loss)"] id3e["Evaluate on Validation Set (RMSE, MAE)"] id4["4. Recursive Forecasting"] id4a["Define Forecast Horizon
(7 days = 10,080 minutes)"] id4b["Initialize Forecast Input
(Last known features & lags)"] id4c["Loop through each future minute:"] id4c1["Create Time Features for Step t"] id4c2["Combine with Updated Lag Features"] id4c3["Predict Power for Step t"] id4c4["Store Prediction"] id4c5["Update Lag Features for Step t+1"] id4d["Compile Forecast Results"] id5["5. Output & Analysis"] id5a["Visualize Forecast vs. History"] id5b["Analyze Feature Importance"] id5c["Assess Potential Error Accumulation"] id5d["Consider Model Limitations & Improvements"]

This structured approach ensures that the data is correctly prepared, features capture relevant information, the model is trained effectively, and the forecasting process is implemented logically.


Understanding the Rankine Cycle Fundamentals

For a deeper understanding of the underlying thermodynamic process being modeled, this video provides a clear explanation of the basic Rankine cycle used in steam power plants.

Video explaining the fundamentals of the Rankine Cycle.

While our simulation doesn't model the physics directly, knowing the cycle components (pump, boiler, turbine, condenser) and how factors like pressure, temperature, and superheating (characteristic of Rankine-Hirn) affect efficiency helps contextualize the power generation patterns the XGBoost model learns from the historical data.


Model Configuration and Feature Summary

The table below summarizes the key configuration choices made in this simulation setup.

Parameter/Feature Type Configuration/Examples Used Rationale
Model Algorithm XGBoost Regressor (`xgb.XGBRegressor`) Effective for tabular data, captures non-linearities and interactions.
Target Variable `power_mw` (Minute-by-minute power) The value we aim to predict.
Time Features Minute, Hour, DayOfWeek, DayOfMonth, Month, DayOfYear Capture cyclical patterns at different time scales.
Lag Features (Minutes) 1, 5, 15, 30, 60, 120, 1440 Provide short-term persistence and daily cycle information from past outputs.
Key Hyperparameters `n_estimators=1000`, `learning_rate=0.05`, `max_depth=7`, `early_stopping_rounds=50` Balances model complexity, training speed, and overfitting prevention. Tunable for specific datasets.
Forecasting Method Recursive Multi-Step Predicts one step at a time, using predictions as inputs for future steps. Necessary for long horizons.
Evaluation Metric (Validation) RMSE, MAE Standard regression metrics to assess one-step-ahead prediction error on unseen data.

These choices provide a solid baseline. For a specific real-world installation, tuning hyperparameters and potentially adding more domain-specific features (like operational modes or ambient temperature, if available) could further improve accuracy.


Frequently Asked Questions (FAQ)

What kind of historical data is needed?

Ideally, you need a consistent time series of the power output (e.g., in kW or MW) recorded at regular minute-by-minute intervals. The longer the history (months or years), the better the model can potentially learn seasonal patterns. Ensure the data has accurate timestamps and minimal missing values. This simulation used 60 days of synthetic data.

How accurate is this type of forecast?

Accuracy depends heavily on data quality, the inherent predictability of the specific Rankine-Hirn cycle's operation, and the forecast horizon. One-step-ahead predictions (like those evaluated on the validation set) are typically more accurate. Recursive multi-step forecasts, especially over long horizons like 7 days, tend to accumulate errors. The MAE/RMSE on the validation set gives an indication, but actual multi-step forecast error will likely be higher. Adding external factors (weather, maintenance schedules) usually improves accuracy significantly.

Can this model handle sudden changes or shutdowns?

Not directly based on the inputs used here (historical power, time, lags). The model learns patterns from the past. If sudden shutdowns or operational changes are not represented in the training data, the model won't anticipate them. To handle such events, you would need additional input features that signal these changes (e.g., operational status flags, scheduled maintenance indicators).

How can I improve this simulation?

Several ways:

  • Use Real Data: Replace synthetic data with actual measurements from your specific installation.
  • Hyperparameter Tuning: Use techniques like Grid Search or Randomized Search with cross-validation (using time-series splits) to find optimal XGBoost parameters.
  • Feature Engineering: Experiment with different lags, rolling statistics (mean, std dev over windows), and interaction features. Consider sine/cosine transformations for cyclical time features.
  • Add External Factors: Incorporate relevant external data like ambient temperature, humidity, grid demand signals, or internal operational parameters if available.
  • Hybrid Models: Combine XGBoost with models better suited for long-term trend extrapolation if a strong underlying trend exists (as mentioned in Answer A).

Recommended Further Exploration


References

direns.minesparis.psl.eu
Rankine / Hirn cycle
direns.minesparis.psl.eu
Steam power plant cycles

Last updated May 3, 2025

AI-assisted research. Sources, methodology and corrections · Source-checked guides and tools · Explore published answers

Ask Ithy AI