am trying to predict the battery health using this ml model XGBoost Regressor i built. 
 I am getting good metrics too, Mean Squared Error: 0.0208, R-squared: 0.9956
but the predicted vs the actual plots looks really messy. there is too much data variation as you can see in the image.  

how can i process the data to make the predict vs actual plot better










# -*- coding: utf-8 -*-
"""Battery_XGBoost.ipynb

Automatically generated by Colab.

Original file is located at
    https://colab.research.google.com/drive/1EmQ4XrYS8wDj9gWaF4Jcjr9T0rRUrdqo
"""

from google.colab import drive
drive.mount('/content/drive/')

import zipfile

zip_ref = zipfile.ZipFile("/content/drive/MyDrive/Data/Batch-5.zip", 'r')
zip_ref.extractall("/content/dataset")
zip_ref.close()

import scipy.io as sio
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score
import xgboost as xgb

# List of .mat files
file_paths = [
    '/content/dataset/Batch-5/RW_battery-1.mat',
    '/content/dataset/Batch-5/RW_battery-2.mat',
    '/content/dataset/Batch-5/RW_battery-3.mat',
    '/content/dataset/Batch-5/RW_battery-4.mat',
    '/content/dataset/Batch-5/RW_battery-5.mat',
    '/content/dataset/Batch-5/RW_battery-6.mat',
    '/content/dataset/Batch-5/RW_battery-7.mat',
    '/content/dataset/Batch-5/RW_battery-8.mat'
]

df_list = []

def process_mat_file(data_file):
    mat_data = sio.loadmat(data_file)
    data_field = mat_data['data']
    extracted_data = []

    for segment in data_field[0]:
        timestamp = [str(t[0][0]) for t in segment[0]]
        voltage = [v[0] for v in segment[1]]
        current = [c[0] for c in segment[2]]
        capacity = [cap[0] for cap in segment[3]]
        charge = [ch[0] for ch in segment[4]]
        discharge = [dis[0] for dis in segment[5]]
        temperature = [temp[0] for temp in segment[6]]
        test_type = [tt[0] for tt in segment[7]]

        max_len = max(len(timestamp), len(voltage), len(current), len(capacity),
                      len(charge), len(discharge), len(temperature), len(test_type))

        timestamp = timestamp[:max_len] + [timestamp[-1]] * (max_len - len(timestamp))
        voltage = voltage[:max_len] + [voltage[-1]] * (max_len - len(voltage))
        current = current[:max_len] + [current[-1]] * (max_len - len(current))
        capacity = capacity[:max_len] + [capacity[-1]] * (max_len - len(capacity))
        charge = charge[:max_len] + [charge[-1]] * (max_len - len(charge))
        discharge = discharge[:max_len] + [discharge[-1]] * (max_len - len(discharge))
        temperature = temperature[:max_len] + [temperature[-1]] * (max_len - len(temperature))
        test_type = test_type[:max_len] + [test_type[-1]] * (max_len - len(test_type))

        segment_data = {
            'Timestamp': timestamp,
            'Voltage': voltage,
            'Current': current,
            'Capacity': capacity,
            'Charge': charge,
            'Discharge': discharge,
            'Temperature': temperature,
            'TestType': test_type
        }

        extracted_data.append(segment_data)

    return pd.concat([pd.DataFrame(segment) for segment in extracted_data], ignore_index=True)
print('Data')

# Load data
for file_path in file_paths:
    print(f"Processing {file_path}...")
    df = process_mat_file(file_path)
    df_list.append(df)

final_df = pd.concat(df_list, ignore_index=True)

# Convert numeric columns
numeric_columns = ['Voltage', 'Current', 'Capacity', 'Charge', 'Discharge', 'Temperature']
for col in numeric_columns:
    final_df[col] = pd.to_numeric(final_df[col], errors='coerce')

# Drop rows with missing values
final_df.dropna(subset=numeric_columns, inplace=True)

# Clean invalid capacity values
final_df = final_df[final_df['Capacity'] >= 0].copy()

# -------------------------------
#  Feature Engineering Section
# -------------------------------
final_df['Power'] = final_df['Voltage'] * final_df['Current']
final_df['Energy'] = final_df['Charge'] * final_df['Voltage']

final_df['CurrentSign'] = np.sign(final_df['Current'])  # charging vs discharging
final_df['Temp_Current_Interaction'] = final_df['Temperature'] * final_df['Current']

# Final feature set
feature_columns = [
    'Voltage', 'Current', 'Charge', 'Discharge', 'Temperature',
    'Power', 'Energy', 'CurrentSign', 'Temp_Current_Interaction'
]
X = final_df[feature_columns]
y = final_df['Capacity']

# Train-test split
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# Final feature set
feature_columns = [
    'Voltage', 'Current', 'Charge', 'Discharge', 'Temperature',
    'Power', 'Energy', 'CurrentSign', 'Temp_Current_Interaction'
]
X = final_df[feature_columns]
y = final_df['Capacity']

# Train-test split
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# XGBoost Regressor with tuned parameters
xg_model = xgb.XGBRegressor(
    n_estimators=200,
    max_depth=5,
    learning_rate=0.05,
    subsample=0.8,
    colsample_bytree=0.8,
    random_state=42
)

# Train
xg_model.fit(X_train, y_train)

# Predict
y_pred_xg = xg_model.predict(X_test)

# Metrics
mse_xg = mean_squared_error(y_test, y_pred_xg)
r2_xg = r2_score(y_test, y_pred_xg)

print(f"XGBoost Regressor - Mean Squared Error: {mse_xg:.4f}")
print(f"XGBoost Regressor - R-squared: {r2_xg:.4f}")

# Actual vs. Predicted Plot
plt.figure(figsize=(8, 6))
plt.scatter(y_test, y_pred_xg, alpha=0.5, color='blue', edgecolors='k')
plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], color='red', linestyle='--')
plt.xlabel('Actual Capacity')
plt.ylabel('Predicted Capacity')
plt.title('XGBoost: Actual vs Predicted Capacity')
plt.grid(True)
plt.tight_layout()
plt.show()

import matplotlib.pyplot as plt

# === VISUALIZATION ===
plt.figure(figsize=(8, 6))

# Scatter plot: using blue with edge color
plt.scatter(
    y_test.values,
    y_pred_xg,
    color='dodgerblue',
    edgecolor='k',
    alpha=0.6,
    label='Predicted',
    s=25
)

# Identity/reference line
plt.plot(
    [min(y_test.values), max(y_test.values)],
    [min(y_test.values), max(y_test.values)],
    color='crimson',
    linestyle='--',
    linewidth=2.5,
    label='Ideal Fit'
)

# Titles and labels with increased font sizes
plt.title('XGBoost - Predicted vs Actual', fontsize=18)
plt.xlabel('Actual Capacity (mAh)', fontsize=16)
plt.ylabel('Predicted Capacity (mAh)', fontsize=16)

# Tick size
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)

# Grid customization
plt.grid(True, linestyle='--', linewidth=0.5, color='gray', dashes=(6, 4))

plt.legend(fontsize=14)

# Increase border width of plot frame
ax = plt.gca()
for spine in ax.spines.values():
    spine.set_linewidth(2)  # Change this number to desired thickness

plt.tight_layout()
plt.savefig("KNN_pred_actual.png",dpi=600)
print("saved")
plt.show()