Compare commits
20
Commits
funny
...
improved_manual
No files matched your search
+4
-1
@@ -184,4 +184,7 @@ ipython_config.py
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
.conda/
|
.conda*/
|
||||||
|
|
||||||
|
data/
|
||||||
|
# for rn
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"ms-python.python",
|
||||||
|
"ms-toolsai.jupyter"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,26 +1,5 @@
|
|||||||
# midterm ml&dm1
|
# midterm ml&dm1
|
||||||
|
|
||||||
## not cool
|
## real
|
||||||
|
|
||||||
currently this shit is a blatant copy. changing soon.
|
not real
|
||||||
|
|
||||||
## how to
|
|
||||||
|
|
||||||
load this thing on google colab dont run on device its so cooked
|
|
||||||
|
|
||||||
how to git? do not `push --force`. prefer pull requests. its like github
|
|
||||||
|
|
||||||
## roadmap
|
|
||||||
|
|
||||||
we will plan to do this in multiple different ways using scikit-learn and pytorch. tensorflow is ancient.
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> phase out tensorflow. dont use it.
|
|
||||||
|
|
||||||
maybe also fit more weather related stats? this dataset is taken from nasa so sources are there
|
|
||||||
|
|
||||||
## collab
|
|
||||||
|
|
||||||
seng tells me this project can be planned without code. basically someone find da waes and other people implements them
|
|
||||||
|
|
||||||
thing is you click [here at scikit learn](https://scikit-learn.org/stable/supervised_learning.html) and tell me how many ways there are you can use to solve a problem. holy fucking shit. too many.
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import io
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
|
def cut_headers(stream: Union[io.StringIO, io.FileIO, io.TextIOWrapper]):
|
||||||
|
"""Remove custom header metadata from the CSV file."""
|
||||||
|
headers = []
|
||||||
|
exist_startheader = False
|
||||||
|
while line := stream.readline():
|
||||||
|
if "-BEGIN HEADER-" in line and not exist_startheader:
|
||||||
|
exist_startheader = True
|
||||||
|
headers.append(str(line.strip()))
|
||||||
|
elif "-END HEADER-" in line and exist_startheader:
|
||||||
|
headers.append(str(line.strip()))
|
||||||
|
break
|
||||||
|
elif exist_startheader:
|
||||||
|
headers.append(str(line.strip()))
|
||||||
|
elif not exist_startheader:
|
||||||
|
continue
|
||||||
|
if not exist_startheader:
|
||||||
|
stream.seek(0)
|
||||||
|
return "", stream
|
||||||
|
return "\n".join(headers), stream
|
||||||
|
|
||||||
|
|
||||||
|
def load_weather_data(file_path: str) -> pd.DataFrame:
|
||||||
|
"""Load weather data and preprocess the datetime index."""
|
||||||
|
with open(file_path, "r", encoding="utf-8") as file:
|
||||||
|
_, csv_file = cut_headers(file)
|
||||||
|
data = pd.read_csv(csv_file, na_values=-999)
|
||||||
|
|
||||||
|
# Convert YEAR, MO, DY to datetime index
|
||||||
|
if all(col in data.columns for col in ("YEAR", "MO", "DY")):
|
||||||
|
dt_conv_table = {
|
||||||
|
k: data[v] for v, k in zip(["YEAR", "MO", "DY"], ["year", "month", "day"])
|
||||||
|
}
|
||||||
|
data["date"] = pd.to_datetime(dt_conv_table)
|
||||||
|
data.drop(columns=["YEAR", "MO", "DY"], inplace=True)
|
||||||
|
data.set_index("date", inplace=True)
|
||||||
|
|
||||||
|
data.dropna(inplace=True)
|
||||||
|
data = data.asfreq("D") # Assumes regular daily data
|
||||||
|
return data
|
||||||
-14429
File diff suppressed because it is too large.
Load diff
+22
-13
@@ -2,9 +2,11 @@ import urllib.parse
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
data_file = "hanoi_weather_data.csv"
|
||||||
|
|
||||||
# Define parameters
|
# Define parameters
|
||||||
latitude, longitude = 21.04815477090267, 105.80123159165096 # lmao
|
latitude, longitude = 21.04815477090267, 105.80123159165096 # lmao
|
||||||
start_date = "2010-01-01"
|
start_date = "1995-01-01"
|
||||||
end_date = datetime.now().strftime("%Y-%m-%d") # Today's date
|
end_date = datetime.now().strftime("%Y-%m-%d") # Today's date
|
||||||
|
|
||||||
# Define desired parameters
|
# Define desired parameters
|
||||||
@@ -16,9 +18,15 @@ end_date = datetime.now().strftime("%Y-%m-%d") # Today's date
|
|||||||
# WS10M MERRA-2 Wind Speed at 10 Meters (m/s)
|
# WS10M MERRA-2 Wind Speed at 10 Meters (m/s)
|
||||||
# WD10M MERRA-2 Wind Direction at 10 Meters (Degrees)
|
# WD10M MERRA-2 Wind Direction at 10 Meters (Degrees)
|
||||||
# more at https://power.larc.nasa.gov/data-access-viewer/
|
# more at https://power.larc.nasa.gov/data-access-viewer/
|
||||||
|
weather_params = [
|
||||||
|
"T2M",
|
||||||
|
"QV2M",
|
||||||
|
"RH2M",
|
||||||
|
"PS",
|
||||||
|
]
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"parameters": "QV2M,T2M,PRECTOTCORR",
|
"parameters": ",".join(weather_params),
|
||||||
"community": "RE",
|
"community": "RE",
|
||||||
"longitude": longitude,
|
"longitude": longitude,
|
||||||
"latitude": latitude,
|
"latitude": latitude,
|
||||||
@@ -36,14 +44,15 @@ base_url = "https://power.larc.nasa.gov/api/temporal/daily/point"
|
|||||||
url = f"{base_url}?{query_string}"
|
url = f"{base_url}?{query_string}"
|
||||||
|
|
||||||
# Make the API request using urllib
|
# Make the API request using urllib
|
||||||
try:
|
if __name__ == "__main__":
|
||||||
with urllib.request.urlopen(url) as response:
|
try:
|
||||||
if response.status == 200:
|
with urllib.request.urlopen(url) as response:
|
||||||
csv_data = response.read().decode("utf-8")
|
if response.status == 200:
|
||||||
with open("hanoi_weather_data.csv", "w", encoding="utf-8") as file:
|
csv_data = response.read()
|
||||||
file.write(csv_data)
|
with open(data_file, "wb") as file:
|
||||||
print("Data successfully downloaded and saved to 'hanoi_weather_data.csv'.")
|
file.write(csv_data)
|
||||||
else:
|
print(f"Data successfully downloaded and saved to {data_file}.")
|
||||||
print(f"Failed to retrieve data: HTTP Status Code {response.status}")
|
else:
|
||||||
except urllib.error.URLError as e:
|
print(f"Failed to retrieve data: HTTP Status Code {response.status}")
|
||||||
print(f"Failed to retrieve data: {e.reason}")
|
except urllib.error.URLError as e:
|
||||||
|
print(f"Failed to retrieve data: {e.reason}")
|
||||||
+11103
File diff suppressed because it is too large.
Load diff
@@ -1,79 +0,0 @@
|
|||||||
"extracting the load data step from the notebook"
|
|
||||||
|
|
||||||
import io
|
|
||||||
|
|
||||||
# from itertools import islice
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
|
|
||||||
def cut_headers(stream: Union[io.StringIO, io.FileIO, io.TextIOWrapper]):
|
|
||||||
headers = []
|
|
||||||
exist_startheader = False
|
|
||||||
|
|
||||||
while line := stream.readline():
|
|
||||||
if "-BEGIN HEADER-" in line and not exist_startheader:
|
|
||||||
exist_startheader = True
|
|
||||||
headers.append(str(line.strip()))
|
|
||||||
elif "-END HEADER-" in line and exist_startheader:
|
|
||||||
headers.append(str(line.strip()))
|
|
||||||
break
|
|
||||||
elif exist_startheader:
|
|
||||||
# print(f"{line = }")
|
|
||||||
headers.append(str(line.strip()))
|
|
||||||
elif not exist_startheader:
|
|
||||||
continue
|
|
||||||
if not exist_startheader:
|
|
||||||
# If no headers are found, reset the stream and return
|
|
||||||
stream.seek(0)
|
|
||||||
return "", stream
|
|
||||||
return "\n".join(headers), stream
|
|
||||||
|
|
||||||
|
|
||||||
# if __name__ == "__main__":
|
|
||||||
# with open("hanoi_qv2m_latest.csv", "r", encoding="utf-8") as file:
|
|
||||||
# header, file = cut_headers(file)
|
|
||||||
# print("Extracted Header:")
|
|
||||||
# print(header)
|
|
||||||
# data = pd.read_csv(file)
|
|
||||||
# print("\nFirst 5 Lines of Data:")
|
|
||||||
# print(data.head())
|
|
||||||
# print(data.isnull().sum())
|
|
||||||
|
|
||||||
|
|
||||||
def load_data(
|
|
||||||
weather_file_name, columns=["Year", "Month", "Day", "Humidity"].copy(), na=-999
|
|
||||||
):
|
|
||||||
with open(weather_file_name, "r", encoding="utf-8") as file:
|
|
||||||
_header, file = cut_headers(file)
|
|
||||||
data = pd.read_csv(file, na_values=na)
|
|
||||||
# print(header)
|
|
||||||
data.columns = columns
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def date_combine(data: pd.DataFrame):
|
|
||||||
data["Date"] = pd.to_datetime(data[["Year", "Month", "Day"]])
|
|
||||||
data = data.drop(columns=["Year", "Month", "Day"])
|
|
||||||
data = data.set_index("Date")
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def validate_view(data: pd.DataFrame):
|
|
||||||
print(f"Shape with N/A values: {data.shape}")
|
|
||||||
isn = data.isnull().sum().iloc[0]
|
|
||||||
print(f"Count of N/A values: {isn}")
|
|
||||||
data = data.dropna() # Drop the rows with missing values
|
|
||||||
isn = data.isnull().sum().iloc[0]
|
|
||||||
print(f"Count after drop: {isn}")
|
|
||||||
print(data.describe())
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
# if __name__ == "__main__":
|
|
||||||
def load_this_data(filename):
|
|
||||||
d = load_data(filename)
|
|
||||||
d = date_combine(d)
|
|
||||||
d = validate_view(d)
|
|
||||||
return d
|
|
||||||
-1161
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,186 @@
|
|||||||
|
# import pandas as pd
|
||||||
|
import os
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
from sklearn.model_selection import train_test_split # type: ignore[import]
|
||||||
|
|
||||||
|
from data_loader import load_weather_data
|
||||||
|
from model_train import (
|
||||||
|
cosine_lr_schedule,
|
||||||
|
# cyclical_lr_schedule,
|
||||||
|
evaluate_model,
|
||||||
|
# exponential_decay_schedule,
|
||||||
|
# linear_decay_schedule,
|
||||||
|
# polynomial_decay_schedule,
|
||||||
|
predict_linear_regression,
|
||||||
|
run_autoregressive_prediction,
|
||||||
|
# step_decay_schedule,
|
||||||
|
train_linear_regression_gd,
|
||||||
|
warmup_schedule,
|
||||||
|
)
|
||||||
|
from preprocessing import (
|
||||||
|
add_rolling_features,
|
||||||
|
create_sequences,
|
||||||
|
inverse_scale,
|
||||||
|
scale_data,
|
||||||
|
)
|
||||||
|
from visualize import (
|
||||||
|
# inspect_residuals,
|
||||||
|
plot_error_hist, # plot_loss_curve,
|
||||||
|
plot_feature_importance,
|
||||||
|
# plot_interactive_time_series,
|
||||||
|
plot_loss_curve_with_annotations,
|
||||||
|
plot_original_vs_scaled,
|
||||||
|
plot_predictions,
|
||||||
|
plot_residual_scatter,
|
||||||
|
plot_residuals,
|
||||||
|
plot_scatter,
|
||||||
|
plot_time_series,
|
||||||
|
plot_value_distribution,
|
||||||
|
visualize_sequence_window,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.ioff()
|
||||||
|
data_file = "hanoi_weather_data.csv"
|
||||||
|
model_file = "theta.npy"
|
||||||
|
|
||||||
|
# Step 1: Load data
|
||||||
|
data = load_weather_data(data_file)
|
||||||
|
|
||||||
|
# Add rolling features
|
||||||
|
data = add_rolling_features(data, window=5)
|
||||||
|
|
||||||
|
plot_value_distribution(data)
|
||||||
|
plot_time_series(data, feature_names=["QV2M"], title="QV2M Time Series")
|
||||||
|
# plot_interactive_time_series(data, feature_names=["QV2M"])
|
||||||
|
# Step 2: Preprocess (scale and sequence)
|
||||||
|
scaled_data, scaler = scale_data(data)
|
||||||
|
plot_original_vs_scaled(data, scaled_data)
|
||||||
|
|
||||||
|
seq_length = 50
|
||||||
|
X, y = create_sequences(scaled_data, seq_length)
|
||||||
|
|
||||||
|
# Flatten X to 2D (n_samples, seq_length * n_features)
|
||||||
|
X = X.reshape(X.shape[0], -1)
|
||||||
|
|
||||||
|
if y.ndim == 1:
|
||||||
|
y = y.reshape(-1, 1) # Single target case
|
||||||
|
|
||||||
|
visualize_sequence_window(data["QV2M"], seq_length=10, start_idx=1000)
|
||||||
|
|
||||||
|
# Step 3: Train/test split
|
||||||
|
test_size = 0.2
|
||||||
|
date_index = data.index[seq_length:]
|
||||||
|
X_train, X_test, y_train, y_test = train_test_split(
|
||||||
|
X, y, test_size=test_size, shuffle=False
|
||||||
|
)
|
||||||
|
|
||||||
|
date_train, date_test = train_test_split(date_index, test_size=test_size, shuffle=False)
|
||||||
|
|
||||||
|
# this is the lr schedulers
|
||||||
|
cos_lr = lambda epoch: cosine_lr_schedule(
|
||||||
|
epoch, max_lr=0.005, min_lr=0.0001, total_epochs=500
|
||||||
|
)
|
||||||
|
# step_lr = lambda epoch: step_decay_schedule(
|
||||||
|
# epoch, initial_lr=0.01, drop=0.5, epochs_drop=100
|
||||||
|
# )
|
||||||
|
# expo_lr = lambda epoch: exponential_decay_schedule(
|
||||||
|
# epoch, initial_lr=0.01, decay_rate=0.96, decay_steps=100
|
||||||
|
# )
|
||||||
|
# linear_lr = lambda epoch: linear_decay_schedule(
|
||||||
|
# epoch, initial_lr=0.01, min_lr=0.001, total_epochs=1000
|
||||||
|
# )
|
||||||
|
# cycle_lr = lambda epoch: cyclical_lr_schedule(
|
||||||
|
# epoch, base_lr=0.001, max_lr=0.01, step_size=200
|
||||||
|
# )
|
||||||
|
# poly_lr = lambda epoch: polynomial_decay_schedule(
|
||||||
|
# epoch, initial_lr=0.005, min_lr=0.0001, total_epochs=1000, power=2
|
||||||
|
# )
|
||||||
|
# warmup_lr = lambda epoch: warmup_schedule(
|
||||||
|
# epoch, warmup_epochs=10, initial_lr=0.001, max_lr=0.01
|
||||||
|
# )
|
||||||
|
|
||||||
|
# Combined learning rate schedule: Warmup + Cosine Annealing
|
||||||
|
combined_lr = lambda epoch: (
|
||||||
|
warmup_schedule(epoch, warmup_epochs=10, initial_lr=0.0001, max_lr=0.005)
|
||||||
|
if epoch < 10
|
||||||
|
else cosine_lr_schedule(epoch - 10, max_lr=0.005, min_lr=0.0001, total_epochs=990)
|
||||||
|
)
|
||||||
|
|
||||||
|
losses = None
|
||||||
|
trained = None
|
||||||
|
if os.path.isfile(model_file):
|
||||||
|
if True or input("found model, train anyway? ").strip().lower() in "y":
|
||||||
|
print("found model but training")
|
||||||
|
trained = False
|
||||||
|
else:
|
||||||
|
print("found model")
|
||||||
|
trained = True
|
||||||
|
else:
|
||||||
|
print("model not found, training")
|
||||||
|
|
||||||
|
# Step 4: Train model with gradient descent
|
||||||
|
if not trained:
|
||||||
|
X_train, X_val, y_train, y_val = train_test_split(
|
||||||
|
X_train, y_train, test_size=0.2, shuffle=False
|
||||||
|
)
|
||||||
|
|
||||||
|
theta, losses, val_losses = train_linear_regression_gd(
|
||||||
|
X_train,
|
||||||
|
y_train,
|
||||||
|
lr_schedule=combined_lr, # Use the combined learning rate schedule
|
||||||
|
n_epochs=1000,
|
||||||
|
batch_size=64,
|
||||||
|
patience=300,
|
||||||
|
l2_reg=0.001, # Adjusted regularization strength
|
||||||
|
val_data=(X_val, y_val), # Pass validation data
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save model
|
||||||
|
np.save(model_file, theta)
|
||||||
|
else:
|
||||||
|
# Load model
|
||||||
|
theta = np.load(model_file)
|
||||||
|
|
||||||
|
predictions = predict_linear_regression(X_test, theta)
|
||||||
|
|
||||||
|
# Step 5: Evaluate
|
||||||
|
mse, r2, mae = evaluate_model(y_test, predictions)
|
||||||
|
print(f"MSE: {mse:.4f}, R2: {r2:.4f}, MAE: {mae:.4f}")
|
||||||
|
|
||||||
|
# Step 6: Rescale predictions
|
||||||
|
y_test_rescaled = inverse_scale(y_test, scaler, y_test.shape[1])
|
||||||
|
predictions_rescaled = inverse_scale(predictions, scaler, y_test.shape[1])
|
||||||
|
|
||||||
|
# Define feature names
|
||||||
|
feature_names = data.columns.tolist()
|
||||||
|
|
||||||
|
# Step 7: Visualize
|
||||||
|
residuals = y_test - predictions
|
||||||
|
errors = abs(residuals)
|
||||||
|
|
||||||
|
plot_predictions(
|
||||||
|
date_test,
|
||||||
|
y_test_rescaled,
|
||||||
|
predictions_rescaled,
|
||||||
|
feature_names=feature_names,
|
||||||
|
title="Actual vs Predicted",
|
||||||
|
)
|
||||||
|
plot_residuals(date_test.to_numpy(), residuals, feature_names=feature_names)
|
||||||
|
plot_error_hist(errors, feature_names=feature_names)
|
||||||
|
plot_scatter(y_test.reshape(-1, 1), predictions.reshape(-1, 1))
|
||||||
|
plot_residual_scatter(y_test, predictions)
|
||||||
|
# == inspect_residuals(predictions, residuals)
|
||||||
|
if losses:
|
||||||
|
plot_loss_curve_with_annotations(losses)
|
||||||
|
if val_losses:
|
||||||
|
plot_loss_curve_with_annotations(val_losses)
|
||||||
|
feature_names = [f"{col}_t-{i}" for col in data.columns for i in range(seq_length)]
|
||||||
|
plot_feature_importance(theta, feature_names=feature_names)
|
||||||
|
|
||||||
|
# Step 8: Autoregressive Prediction
|
||||||
|
autoregressive_predictions = run_autoregressive_prediction(
|
||||||
|
theta, X_test, y_test, scaler, date_test, data.shape[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.ion()
|
||||||
+350
@@ -0,0 +1,350 @@
|
|||||||
|
# from itertools import islice
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
from sklearn.metrics import ( # type: ignore[import]
|
||||||
|
mean_absolute_error,
|
||||||
|
mean_squared_error,
|
||||||
|
r2_score,
|
||||||
|
)
|
||||||
|
|
||||||
|
from preprocessing import inverse_scale
|
||||||
|
|
||||||
|
|
||||||
|
def train_linear_regression_gd(
|
||||||
|
X_train,
|
||||||
|
y_train,
|
||||||
|
lr_schedule=lambda _e: 0.01,
|
||||||
|
n_epochs=1000,
|
||||||
|
batch_size=32,
|
||||||
|
patience=20,
|
||||||
|
l2_reg=0.0,
|
||||||
|
l1_reg=0.0,
|
||||||
|
val_data=None, # Add validation data
|
||||||
|
):
|
||||||
|
# Ensure X_train is 2D
|
||||||
|
assert X_train.ndim == 2, "X_train must be 2D (n_samples, seq_length * n_features)."
|
||||||
|
|
||||||
|
# Ensure y_train is 2D
|
||||||
|
if y_train.ndim == 1:
|
||||||
|
y_train = y_train.reshape(-1, 1) # Handle single target case
|
||||||
|
|
||||||
|
m, n = X_train.shape
|
||||||
|
n_targets = y_train.shape[1] # Number of target variables
|
||||||
|
|
||||||
|
# Add bias term
|
||||||
|
X_b = np.hstack([np.ones((m, 1)), X_train]) # Add bias term
|
||||||
|
|
||||||
|
# Initialize theta with small random values
|
||||||
|
theta = np.random.randn(n + 1, n_targets) * 0.01
|
||||||
|
losses = []
|
||||||
|
val_losses = [] # Track validation losses
|
||||||
|
|
||||||
|
best_loss = float("inf")
|
||||||
|
patience_counter = 0
|
||||||
|
|
||||||
|
X_val, y_val = val_data if val_data else (None, None)
|
||||||
|
if X_val is not None:
|
||||||
|
X_val_b = np.hstack(
|
||||||
|
[np.ones((X_val.shape[0], 1)), X_val]
|
||||||
|
) # Add bias term to validation data
|
||||||
|
|
||||||
|
for epoch in range(n_epochs):
|
||||||
|
start_time = time.time() # Start timing the epoch
|
||||||
|
|
||||||
|
# Shuffle the data at the start of each epoch
|
||||||
|
indices = np.random.permutation(m)
|
||||||
|
X_b_shuffled = X_b[indices]
|
||||||
|
y_shuffled = y_train[indices]
|
||||||
|
|
||||||
|
epoch_loss = 0.0
|
||||||
|
gradient_norm = 0.0
|
||||||
|
|
||||||
|
for i in range(0, m, batch_size):
|
||||||
|
X_batch = X_b_shuffled[i : i + batch_size]
|
||||||
|
y_batch = y_shuffled[i : i + batch_size]
|
||||||
|
|
||||||
|
lr = lr_schedule(epoch)
|
||||||
|
predictions = X_batch @ theta # Shape: (batch_size, n_targets)
|
||||||
|
errors = predictions - y_batch # Shape: (batch_size, n_targets)
|
||||||
|
|
||||||
|
# Compute gradients with regularization
|
||||||
|
gradients = (
|
||||||
|
(2 / batch_size) * X_batch.T @ errors
|
||||||
|
) # Shape: (n_features + 1, n_targets)
|
||||||
|
if l2_reg > 0:
|
||||||
|
gradients[1:] += (
|
||||||
|
2 * l2_reg * theta[1:]
|
||||||
|
) # Apply L2 regularization (skip bias)
|
||||||
|
if l1_reg > 0:
|
||||||
|
gradients[1:] += l1_reg * np.sign(
|
||||||
|
theta[1:]
|
||||||
|
) # Apply L1 regularization (skip bias)
|
||||||
|
|
||||||
|
# Clip gradients to prevent exploding gradients
|
||||||
|
clip_value = 1e3
|
||||||
|
gradients = np.clip(gradients, -clip_value, clip_value)
|
||||||
|
|
||||||
|
gradient_norm += np.linalg.norm(gradients) # Track gradient norm
|
||||||
|
theta -= lr * gradients # Update theta
|
||||||
|
|
||||||
|
batch_loss = np.mean(errors**2) # Compute batch loss
|
||||||
|
if l2_reg > 0:
|
||||||
|
batch_loss += l2_reg * np.sum(theta[1:] ** 2) # Add L2 penalty
|
||||||
|
if l1_reg > 0:
|
||||||
|
batch_loss += l1_reg * np.sum(np.abs(theta[1:])) # Add L1 penalty
|
||||||
|
|
||||||
|
epoch_loss += batch_loss
|
||||||
|
|
||||||
|
epoch_loss /= m // batch_size
|
||||||
|
losses.append(epoch_loss)
|
||||||
|
|
||||||
|
# Compute validation loss
|
||||||
|
if X_val is not None:
|
||||||
|
val_predictions = X_val_b @ theta
|
||||||
|
val_loss = np.mean((val_predictions - y_val) ** 2)
|
||||||
|
val_losses.append(val_loss)
|
||||||
|
|
||||||
|
# Check for early stopping
|
||||||
|
if epoch_loss < best_loss:
|
||||||
|
best_loss = epoch_loss
|
||||||
|
patience_counter = 0
|
||||||
|
else:
|
||||||
|
patience_counter += 1
|
||||||
|
|
||||||
|
if patience_counter >= patience:
|
||||||
|
print(f"Early stopping at epoch {epoch}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Log statistics
|
||||||
|
epoch_time = time.time() - start_time
|
||||||
|
if epoch % 50 == 0 or epoch == n_epochs - 1:
|
||||||
|
val_loss_str = (
|
||||||
|
f", Val Loss = {val_losses[-1]:.6f}" if X_val is not None else ""
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"Epoch {epoch}: Loss = {epoch_loss:.6f}{val_loss_str}, LR: {lr:.6f}, "
|
||||||
|
f"Gradient Norm = {gradient_norm:.4f}, Epoch Time = {epoch_time:.2f}s"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Final Loss: {losses[-1]:.6f}")
|
||||||
|
if X_val is not None:
|
||||||
|
print(f"Final Validation Loss: {val_losses[-1]:.6f}")
|
||||||
|
return theta, losses, val_losses
|
||||||
|
return theta, losses, None
|
||||||
|
|
||||||
|
|
||||||
|
def predict_linear_regression(X, theta):
|
||||||
|
"""
|
||||||
|
Predict using the linear regression model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
X: Input features (2D or 3D array).
|
||||||
|
theta: Model weights.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
predictions: Predicted values.
|
||||||
|
"""
|
||||||
|
X_b = np.hstack([np.ones((X.shape[0], 1)), X]) # Add bias term
|
||||||
|
return X_b @ theta
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_model(y_true, y_pred):
|
||||||
|
"""Evaluate using sklearn's built-in MSE and R² functions."""
|
||||||
|
mse = mean_squared_error(y_true, y_pred)
|
||||||
|
r2 = r2_score(y_true, y_pred)
|
||||||
|
mae = mean_absolute_error(y_true, y_pred)
|
||||||
|
return iter((mse, r2, mae))
|
||||||
|
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
def cosine_lr_schedule(epoch, max_lr=0.01, min_lr=0.001, total_epochs=1000):
|
||||||
|
return (
|
||||||
|
min_lr + (max_lr - min_lr) * (1 + math.cos(math.pi * epoch / total_epochs)) / 2
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def step_decay_schedule(epoch, initial_lr=0.01, drop=0.5, epochs_drop=50):
|
||||||
|
"""
|
||||||
|
Step decay learning rate scheduler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
epoch: Current epoch.
|
||||||
|
initial_lr: Initial learning rate.
|
||||||
|
drop: Factor by which to reduce the learning rate.
|
||||||
|
epochs_drop: Number of epochs after which to reduce the learning rate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updated learning rate.
|
||||||
|
"""
|
||||||
|
return initial_lr * (drop ** (epoch // epochs_drop))
|
||||||
|
|
||||||
|
|
||||||
|
def exponential_decay_schedule(epoch, initial_lr=0.01, decay_rate=0.1, decay_steps=100):
|
||||||
|
"""
|
||||||
|
Exponential decay learning rate scheduler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
epoch: Current epoch.
|
||||||
|
initial_lr: Initial learning rate.
|
||||||
|
decay_rate: Rate of decay.
|
||||||
|
decay_steps: Number of steps for decay.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updated learning rate.
|
||||||
|
"""
|
||||||
|
return initial_lr * (decay_rate ** (epoch / decay_steps))
|
||||||
|
|
||||||
|
|
||||||
|
def linear_decay_schedule(epoch, initial_lr=0.01, min_lr=0.001, total_epochs=1000):
|
||||||
|
"""
|
||||||
|
Linear decay learning rate scheduler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
epoch: Current epoch.
|
||||||
|
initial_lr: Initial learning rate.
|
||||||
|
min_lr: Minimum learning rate.
|
||||||
|
total_epochs: Total number of epochs.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updated learning rate.
|
||||||
|
"""
|
||||||
|
return max(min_lr, initial_lr - (initial_lr - min_lr) * (epoch / total_epochs))
|
||||||
|
|
||||||
|
|
||||||
|
def cyclical_lr_schedule(epoch, base_lr=0.001, max_lr=0.01, step_size=200):
|
||||||
|
"""
|
||||||
|
Cyclical learning rate scheduler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
epoch: Current epoch.
|
||||||
|
base_lr: Minimum learning rate.
|
||||||
|
max_lr: Maximum learning rate.
|
||||||
|
step_size: Number of epochs for half a cycle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updated learning rate.
|
||||||
|
"""
|
||||||
|
cycle = np.floor(1 + epoch / (2 * step_size))
|
||||||
|
x = np.abs(epoch / step_size - 2 * cycle + 1)
|
||||||
|
return base_lr + (max_lr - base_lr) * max(0, (1 - x))
|
||||||
|
|
||||||
|
|
||||||
|
def polynomial_decay_schedule(
|
||||||
|
epoch, initial_lr=0.01, min_lr=0.001, total_epochs=1000, power=2
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Polynomial decay learning rate scheduler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
epoch: Current epoch.
|
||||||
|
initial_lr: Initial learning rate.
|
||||||
|
min_lr: Minimum learning rate.
|
||||||
|
total_epochs: Total number of epochs.
|
||||||
|
power: Polynomial power.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updated learning rate.
|
||||||
|
"""
|
||||||
|
return min_lr + (initial_lr - min_lr) * ((1 - epoch / total_epochs) ** power)
|
||||||
|
|
||||||
|
|
||||||
|
def warmup_schedule(epoch, warmup_epochs=10, initial_lr=0.01, max_lr=0.1):
|
||||||
|
"""
|
||||||
|
Warm-up learning rate scheduler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
epoch: Current epoch.
|
||||||
|
warmup_epochs: Number of warm-up epochs.
|
||||||
|
initial_lr: Initial learning rate.
|
||||||
|
max_lr: Maximum learning rate after warm-up.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updated learning rate.
|
||||||
|
"""
|
||||||
|
if epoch < warmup_epochs:
|
||||||
|
return initial_lr + (max_lr - initial_lr) * (epoch / warmup_epochs)
|
||||||
|
return max_lr
|
||||||
|
|
||||||
|
|
||||||
|
def autoregressive_prediction(model, initial_sequence, steps, scaler, n_features):
|
||||||
|
"""
|
||||||
|
Perform autoregressive predictions using the model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: The trained model (theta for linear regression).
|
||||||
|
initial_sequence: The initial sequence to start predictions (from test data).
|
||||||
|
steps: Number of steps to predict.
|
||||||
|
scaler: Scaler used for inverse scaling.
|
||||||
|
n_features: Number of features in the data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
predictions: List of predicted values.
|
||||||
|
"""
|
||||||
|
sequence = initial_sequence.copy() # Start with the initial sequence
|
||||||
|
predictions = []
|
||||||
|
|
||||||
|
for _ in range(steps):
|
||||||
|
# Predict the next value
|
||||||
|
next_prediction = predict_linear_regression(
|
||||||
|
sequence.reshape(1, -1), model
|
||||||
|
) # Shape: (1, n_targets)
|
||||||
|
predictions.append(next_prediction[0]) # Append the prediction (flattened)
|
||||||
|
|
||||||
|
# Update the sequence: Remove the oldest timestep and append the new prediction
|
||||||
|
sequence = np.roll(sequence, -n_features) # Shift sequence left by n_features
|
||||||
|
sequence[-n_features:] = (
|
||||||
|
next_prediction # Add the new prediction (entire target vector)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Rescale predictions back to original scale
|
||||||
|
predictions_rescaled = inverse_scale(np.array(predictions), scaler, n_features)
|
||||||
|
return predictions_rescaled
|
||||||
|
|
||||||
|
|
||||||
|
def run_autoregressive_prediction(
|
||||||
|
model, X_test, y_test, scaler, date_test, n_features, steps=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Run autoregressive prediction and visualize the results.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: The trained model (theta for linear regression).
|
||||||
|
X_test: Test features.
|
||||||
|
y_test: Test labels.
|
||||||
|
scaler: Scaler used for inverse scaling.
|
||||||
|
date_test: Dates corresponding to the test set.
|
||||||
|
n_features: Number of features in the data.
|
||||||
|
steps: Number of steps to predict (default is the length of y_test).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
autoregressive_predictions: Rescaled autoregressive predictions.
|
||||||
|
"""
|
||||||
|
if steps is None:
|
||||||
|
steps = len(y_test) # Default to the length of the test set
|
||||||
|
|
||||||
|
# Use the first test sequence as the initial sequence
|
||||||
|
|
||||||
|
initial_sequence = X_test[0]
|
||||||
|
|
||||||
|
# Perform autoregressive prediction
|
||||||
|
autoregressive_predictions = autoregressive_prediction(
|
||||||
|
model, initial_sequence, steps, scaler, n_features
|
||||||
|
)
|
||||||
|
|
||||||
|
# Visualize the results
|
||||||
|
plt.figure(figsize=(12, 6))
|
||||||
|
plt.plot(date_test, inverse_scale(y_test, scaler, n_features), label="Actual")
|
||||||
|
plt.plot(date_test, autoregressive_predictions, label="Autoregressive Predictions")
|
||||||
|
plt.title("Autoregressive Predictions vs Actual")
|
||||||
|
plt.xlabel("Date")
|
||||||
|
plt.ylabel("Value")
|
||||||
|
plt.legend()
|
||||||
|
plt.grid(True)
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
return autoregressive_predictions
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from sklearn.preprocessing import MinMaxScaler # type: ignore[import]
|
||||||
|
|
||||||
|
# from sklearn.model_selection import train_test_split
|
||||||
|
|
||||||
|
|
||||||
|
def scale_data(data):
|
||||||
|
"""Normalize the data using MinMaxScaler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: DataFrame or array with multiple columns.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
scaled_data: Scaled data as a NumPy array.
|
||||||
|
scaler: Fitted MinMaxScaler object.
|
||||||
|
"""
|
||||||
|
if isinstance(data, np.ndarray):
|
||||||
|
if data.ndim == 1:
|
||||||
|
data = data.reshape(-1, 1) # Ensure 2D for single-column arrays
|
||||||
|
else:
|
||||||
|
data = data.to_numpy() # Convert DataFrame to NumPy array
|
||||||
|
|
||||||
|
scaler = MinMaxScaler()
|
||||||
|
scaled_data = scaler.fit_transform(data)
|
||||||
|
return scaled_data, scaler
|
||||||
|
|
||||||
|
|
||||||
|
def inverse_scale(scaled, scaler, n_features):
|
||||||
|
"""Inverse transform using stored scaler."""
|
||||||
|
return scaler.inverse_transform(scaled.reshape(-1, n_features))
|
||||||
|
|
||||||
|
|
||||||
|
def create_sequences(data, seq_length):
|
||||||
|
"""
|
||||||
|
Create sequences for time-series data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Scaled data as a NumPy array (multi-column supported).
|
||||||
|
seq_length: Length of each sequence.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
X: Input sequences (3D array).
|
||||||
|
y: Target values (2D array for multi-column, 1D for single-column).
|
||||||
|
"""
|
||||||
|
X, y = [], []
|
||||||
|
for i in range(len(data) - seq_length):
|
||||||
|
X.append(data[i : i + seq_length])
|
||||||
|
y.append(data[i + seq_length]) # Automatically handles multi-column
|
||||||
|
return np.array(X), np.array(y)
|
||||||
|
|
||||||
|
|
||||||
|
def add_rolling_features(data, window=5):
|
||||||
|
"""
|
||||||
|
Add rolling mean and standard deviation features to the dataset.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: DataFrame containing the original features.
|
||||||
|
window: Window size for rolling calculations.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame with additional rolling features.
|
||||||
|
"""
|
||||||
|
rolling_data = data.copy()
|
||||||
|
for col in data.columns:
|
||||||
|
rolling_data[f"{col}_rolling_mean_{window}"] = (
|
||||||
|
data[col].rolling(window=window).mean()
|
||||||
|
)
|
||||||
|
rolling_data[f"{col}_rolling_std_{window}"] = (
|
||||||
|
data[col].rolling(window=window).std()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Drop rows with NaN values introduced by rolling calculations
|
||||||
|
rolling_data = rolling_data.dropna()
|
||||||
|
return rolling_data
|
||||||
+323
@@ -0,0 +1,323 @@
|
|||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
# data exploration
|
||||||
|
|
||||||
|
|
||||||
|
def plot_value_distribution(data):
|
||||||
|
# import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
plt.figure(figsize=(8, 4))
|
||||||
|
plt.hist(data.iloc[:, 0], bins=40, color="skyblue", edgecolor="black")
|
||||||
|
plt.title("Distribution of Values")
|
||||||
|
plt.xlabel(data.columns[0])
|
||||||
|
plt.ylabel("Frequency")
|
||||||
|
plt.grid(True)
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def plot_time_series(
|
||||||
|
data, feature_names=None, title="Time Series Trends", xlabel="Date", ylabel="Value"
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Plot time series data for specified features.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: DataFrame containing the time series data.
|
||||||
|
feature_names: List of feature names to plot. If None, all features are plotted.
|
||||||
|
title: Title of the plot.
|
||||||
|
xlabel: Label for the x-axis.
|
||||||
|
ylabel: Label for the y-axis.
|
||||||
|
"""
|
||||||
|
if feature_names is None:
|
||||||
|
feature_names = data.columns
|
||||||
|
|
||||||
|
plt.figure(figsize=(12, 6))
|
||||||
|
for feature in feature_names:
|
||||||
|
plt.plot(data.index, data[feature], label=feature)
|
||||||
|
plt.title(title)
|
||||||
|
plt.xlabel(xlabel)
|
||||||
|
plt.ylabel(ylabel)
|
||||||
|
plt.legend()
|
||||||
|
plt.grid(True)
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def plot_original_vs_scaled(original_data, scaled_data, feature_names=None):
|
||||||
|
"""
|
||||||
|
Plot original and scaled data, aligned by date.
|
||||||
|
"""
|
||||||
|
if feature_names is None:
|
||||||
|
feature_names = original_data.columns[:3] # demo
|
||||||
|
|
||||||
|
dates = original_data.index # Grab the datetime index
|
||||||
|
|
||||||
|
fig, axs = plt.subplots(2, 1, figsize=(12, 6), sharex=True)
|
||||||
|
|
||||||
|
for col in feature_names:
|
||||||
|
axs[0].plot(dates, original_data[col], label=f"Original {col}")
|
||||||
|
axs[0].set_title("Original Data")
|
||||||
|
axs[0].legend()
|
||||||
|
axs[0].grid(True)
|
||||||
|
|
||||||
|
for i, col in enumerate(feature_names):
|
||||||
|
col_index = original_data.columns.get_loc(col)
|
||||||
|
axs[1].plot(dates, scaled_data[:, col_index], label=f"Scaled {col}")
|
||||||
|
axs[1].set_title("Scaled Data (MinMaxScaler)")
|
||||||
|
axs[1].legend()
|
||||||
|
axs[1].grid(True)
|
||||||
|
|
||||||
|
plt.xlabel("Date")
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def visualize_sequence_window(series, seq_length=5, start_idx=0):
|
||||||
|
"""
|
||||||
|
Visualize how a sequence window is used to create X (input) and y (target).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
series (pd.Series or np.ndarray): 1D time series data
|
||||||
|
seq_length (int): Length of input window
|
||||||
|
start_idx (int): Where to start slicing from for demo
|
||||||
|
"""
|
||||||
|
if isinstance(series, np.ndarray):
|
||||||
|
data = series
|
||||||
|
else:
|
||||||
|
data = series.to_numpy()
|
||||||
|
|
||||||
|
x_seq = data[start_idx : start_idx + seq_length]
|
||||||
|
y_val = data[start_idx + seq_length]
|
||||||
|
|
||||||
|
full_seq = np.concatenate([x_seq, [y_val]])
|
||||||
|
|
||||||
|
plt.figure(figsize=(8, 3))
|
||||||
|
plt.plot(range(start_idx, start_idx + seq_length + 1), full_seq, marker="o")
|
||||||
|
plt.axvline(x=start_idx + seq_length - 0.5, linestyle="--", color="gray")
|
||||||
|
|
||||||
|
for i in range(seq_length):
|
||||||
|
plt.text(
|
||||||
|
start_idx + i, x_seq[i], f"x[{i}]", ha="center", va="bottom", fontsize=9
|
||||||
|
)
|
||||||
|
plt.text(
|
||||||
|
start_idx + seq_length,
|
||||||
|
y_val,
|
||||||
|
"y",
|
||||||
|
ha="center",
|
||||||
|
va="bottom",
|
||||||
|
color="red",
|
||||||
|
fontsize=9,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.title(f"Sequence Window (seq_length={seq_length}) → Predict Next")
|
||||||
|
plt.xlabel("Time Step")
|
||||||
|
plt.ylabel("Value")
|
||||||
|
plt.grid(True)
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
# training analysis
|
||||||
|
|
||||||
|
|
||||||
|
def plot_loss_curve_with_annotations(losses, annotate_min=True):
|
||||||
|
"""
|
||||||
|
Plot the training loss curve with optional annotations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
losses: List of loss values for each epoch.
|
||||||
|
annotate_min: Whether to annotate the minimum loss point.
|
||||||
|
"""
|
||||||
|
plt.figure(figsize=(10, 6))
|
||||||
|
plt.plot(losses, label="Loss")
|
||||||
|
if annotate_min:
|
||||||
|
min_loss_idx = np.argmin(losses)
|
||||||
|
plt.scatter(
|
||||||
|
min_loss_idx,
|
||||||
|
losses[min_loss_idx],
|
||||||
|
color="red",
|
||||||
|
label=f"Min Loss: {losses[min_loss_idx]:.4f}",
|
||||||
|
)
|
||||||
|
plt.title("Training Loss Curve")
|
||||||
|
plt.xlabel("Epoch")
|
||||||
|
plt.ylabel("MSE Loss")
|
||||||
|
plt.legend()
|
||||||
|
plt.grid(True)
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
# post training evaluation
|
||||||
|
|
||||||
|
|
||||||
|
def plot_predictions(
|
||||||
|
date_test,
|
||||||
|
y_test_rescaled,
|
||||||
|
predictions_rescaled,
|
||||||
|
feature_names=None,
|
||||||
|
title="Actual vs Predicted",
|
||||||
|
xlabel="Date",
|
||||||
|
ylabel="Value",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Plot actual vs predicted values for each column separately.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
date_test: Array of test dates.
|
||||||
|
y_test_rescaled: Rescaled actual values.
|
||||||
|
predictions_rescaled: Rescaled predicted values.
|
||||||
|
feature_names: List of feature names corresponding to columns.
|
||||||
|
title: Title of the plot.
|
||||||
|
xlabel: Label for the x-axis.
|
||||||
|
ylabel: Label for the y-axis.
|
||||||
|
"""
|
||||||
|
n_columns = y_test_rescaled.shape[1]
|
||||||
|
if feature_names is None:
|
||||||
|
feature_names = [f"Column {i}" for i in range(n_columns)]
|
||||||
|
|
||||||
|
for i in range(n_columns):
|
||||||
|
plt.figure(figsize=(12, 6))
|
||||||
|
plt.plot(date_test, y_test_rescaled[:, i], label=f"Actual ({feature_names[i]})")
|
||||||
|
plt.plot(
|
||||||
|
date_test,
|
||||||
|
predictions_rescaled[:, i],
|
||||||
|
label=f"Predicted ({feature_names[i]})",
|
||||||
|
)
|
||||||
|
plt.title(f"{title} ({feature_names[i]})")
|
||||||
|
plt.xlabel(xlabel)
|
||||||
|
plt.ylabel(ylabel)
|
||||||
|
plt.xticks(rotation=45)
|
||||||
|
plt.legend()
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.grid(True)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def plot_residuals(date_test, residuals, feature_names=None):
|
||||||
|
"""
|
||||||
|
Plot residuals over time for each column separately.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
date_test: Array of test dates.
|
||||||
|
residuals: Array of residuals (n_samples, n_columns).
|
||||||
|
feature_names: List of feature names corresponding to columns.
|
||||||
|
"""
|
||||||
|
n_columns = residuals.shape[1]
|
||||||
|
if feature_names is None:
|
||||||
|
feature_names = [f"Column {i}" for i in range(n_columns)]
|
||||||
|
|
||||||
|
for i in range(n_columns):
|
||||||
|
plt.figure(figsize=(10, 4))
|
||||||
|
plt.plot(date_test, residuals[:, i], label=f"Residuals ({feature_names[i]})")
|
||||||
|
plt.hlines(0, date_test.min(), date_test.max(), colors="red", linestyles="--")
|
||||||
|
plt.title(f"Residuals Over Time ({feature_names[i]})")
|
||||||
|
plt.xlabel("Date")
|
||||||
|
plt.ylabel("Error")
|
||||||
|
plt.xticks(rotation=45)
|
||||||
|
plt.grid(True)
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.legend()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def plot_error_hist(errors, feature_names=None):
|
||||||
|
"""
|
||||||
|
Plot error histograms for each column separately.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
errors: Array of errors (n_samples, n_columns).
|
||||||
|
feature_names: List of feature names corresponding to columns.
|
||||||
|
"""
|
||||||
|
n_columns = errors.shape[1]
|
||||||
|
if feature_names is None:
|
||||||
|
feature_names = [f"Column {i}" for i in range(n_columns)]
|
||||||
|
|
||||||
|
for i in range(n_columns):
|
||||||
|
plt.figure(figsize=(8, 4))
|
||||||
|
plt.hist(errors[:, i], bins=50, alpha=0.7, label=f"{feature_names[i]}")
|
||||||
|
plt.title(f"Error Distribution ({feature_names[i]})")
|
||||||
|
plt.xlabel("Absolute Error")
|
||||||
|
plt.ylabel("Frequency")
|
||||||
|
plt.grid(True)
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def plot_scatter(y_test, predictions):
|
||||||
|
plt.scatter(y_test[:, 0], predictions[:, 0], alpha=0.5)
|
||||||
|
plt.plot([0, 1], [0, 1], "r--")
|
||||||
|
plt.title("Predicted vs Actual (Normalized)")
|
||||||
|
plt.xlabel("Actual")
|
||||||
|
plt.ylabel("Predicted")
|
||||||
|
plt.grid(True)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def plot_residual_scatter(y_test, predictions):
|
||||||
|
residuals = y_test - predictions
|
||||||
|
plt.figure(figsize=(8, 6))
|
||||||
|
plt.scatter(predictions, residuals, alpha=0.5)
|
||||||
|
plt.axhline(0, color="red", linestyle="--")
|
||||||
|
plt.title("Residuals vs Predictions")
|
||||||
|
plt.xlabel("Predicted Values")
|
||||||
|
plt.ylabel("Residuals")
|
||||||
|
plt.grid(True)
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def plot_feature_importance(theta, feature_names):
|
||||||
|
"""
|
||||||
|
Plot feature importance based on learned weights.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
theta: Array of weights (including bias) with shape (n_features + 1, n_targets).
|
||||||
|
feature_names: List of feature names.
|
||||||
|
"""
|
||||||
|
if feature_names is None:
|
||||||
|
feature_names = [f"Feature {i}" for i in range(theta.shape[0] - 1)]
|
||||||
|
|
||||||
|
if theta.shape[0] != len(feature_names) + 1:
|
||||||
|
raise ValueError(
|
||||||
|
f"Mismatch: theta has {theta.shape[0]} rows (including bias), "
|
||||||
|
f"but feature_names has {len(feature_names)} elements."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Average weights across all targets
|
||||||
|
avg_theta = np.mean(theta, axis=1)
|
||||||
|
|
||||||
|
plt.figure(figsize=(10, 6))
|
||||||
|
plt.bar(range(len(avg_theta)), avg_theta, tick_label=["Bias"] + feature_names)
|
||||||
|
plt.xticks(rotation=45, ha="right") # Rotate labels 45 degrees
|
||||||
|
plt.title("Feature Importance (Averaged Across Targets)")
|
||||||
|
plt.xlabel("Features")
|
||||||
|
plt.ylabel("Weight")
|
||||||
|
plt.grid(True)
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
# import plotly.express as px
|
||||||
|
|
||||||
|
|
||||||
|
# def plot_interactive_time_series(data, feature_names=None):
|
||||||
|
# if feature_names is None:
|
||||||
|
# feature_names = data.columns
|
||||||
|
|
||||||
|
# fig = px.line(data, x=data.index, y=feature_names, title="Interactive Time Series")
|
||||||
|
# fig.show()
|
||||||
|
|
||||||
|
|
||||||
|
def inspect_residuals(predictions, residuals):
|
||||||
|
plt.figure(figsize=(10, 6))
|
||||||
|
plt.scatter(predictions, residuals, alpha=0.5)
|
||||||
|
plt.axhline(0, color="red", linestyle="--")
|
||||||
|
plt.title("Residuals vs Predictions")
|
||||||
|
plt.xlabel("Predicted Values")
|
||||||
|
plt.ylabel("Residuals")
|
||||||
|
plt.grid(True)
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.show()
|
||||||
Reference in new issue
Block a user