-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathanalysis.py
More file actions
2158 lines (1806 loc) · 95 KB
/
Copy pathanalysis.py
File metadata and controls
2158 lines (1806 loc) · 95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# ========================= Import Necessary Libraries =========================
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.linear_model import Ridge, Lasso
from sklearn.svm import SVR
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, r2_score
from scipy.stats import pearsonr, spearmanr, kendalltau
from lifelines import KaplanMeierFitter, CoxPHFitter
from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.arima.model import ARIMA
from datetime import timedelta
import warnings
from tqdm import tqdm
import random
import os # For directory management
import datetime # For timestamp generation
# ========================= Setup and Configuration =========================
# Suppress warnings for cleaner output
warnings.filterwarnings("ignore")
# Set random seed for reproducibility
def set_seed(seed=42):
"""
Sets the random seed for reproducibility.
Parameters:
- seed (int): The seed value.
"""
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
set_seed()
# ========================= Helper Functions =========================
def get_timestamp():
"""
Generates a timestamp string in the format YYYYMMDD_HHMMSS.
Returns:
- str: The current timestamp.
"""
return datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
# Ensure the 'plots' directory exists
os.makedirs('plots', exist_ok=True)
# ========================= Clustering Analysis =========================
class ClusteringAnalysis:
def __init__(self, data: pd.DataFrame, feature_columns: list, customer_id_column: str = 'customer_id'):
"""
Initializes the ClusteringAnalysis class with features for clustering.
Parameters:
- data (pd.DataFrame): DataFrame containing customer data, including 'customer_id' and other features.
- feature_columns (list of str): List of column names to use for clustering.
- customer_id_column (str): Column name for customer IDs.
"""
self.df = data.copy()
self.feature_columns = feature_columns
self.customer_id_column = customer_id_column
self.scaled_features = None # To store scaled features used for clustering
# Verify that the selected features exist in the DataFrame
missing_features = [feature for feature in self.feature_columns if feature not in self.df.columns]
if missing_features:
raise ValueError(f"The following feature columns are missing from the data: {missing_features}")
def _scale_features(self):
"""
Scales the selected features using StandardScaler.
This is crucial for algorithms like KMeans and DBSCAN that are sensitive to feature scaling.
"""
scaler = StandardScaler()
self.scaled_features = scaler.fit_transform(self.df[self.feature_columns])
def kmeans_cluster(self, n_clusters: int = 3, random_state: int = 42, visualize: bool = True):
"""
Applies KMeans clustering to the data.
Parameters:
- n_clusters (int): The number of clusters to form.
- random_state (int): Determines random number generation for centroid initialization.
- visualize (bool): Whether to visualize the clustering results.
Returns:
- dict: Contains a notification that the plot has been generated and a short summary.
"""
self._scale_features()
kmeans = KMeans(n_clusters=n_clusters, random_state=random_state)
self.df['cluster_kmeans'] = kmeans.fit_predict(self.scaled_features)
summary = f"KMeans clustering performed with {n_clusters} clusters."
if visualize:
filename = self.plot_clusters('cluster_kmeans', title='KMeans Clustering Results')
summary += " A clustering visualization plot has been generated."
return {"file_path": filename, "summary": summary}
else:
return {"file_path": None, "summary": summary}
def dbscan_cluster(self, eps: float = 0.5, min_samples: int = 5, visualize: bool = True):
"""
Applies DBSCAN clustering to the data.
Parameters:
- eps (float): The maximum distance between two samples for one to be considered as in the neighborhood of the other.
- min_samples (int): The number of samples in a neighborhood for a point to be considered as a core point.
- visualize (bool): Whether to visualize the clustering results.
Returns:
- dict: Contains a notification that the plot has been generated and a short summary.
"""
self._scale_features()
dbscan = DBSCAN(eps=eps, min_samples=min_samples)
self.df['cluster_dbscan'] = dbscan.fit_predict(self.scaled_features)
num_clusters = len(set(self.df['cluster_dbscan'])) - (1 if -1 in self.df['cluster_dbscan'] else 0)
summary = f"DBSCAN clustering performed with eps={eps} and min_samples={min_samples}. Number of clusters found: {num_clusters}."
if visualize:
filename = self.plot_clusters('cluster_dbscan', title='DBSCAN Clustering Results')
summary += " A clustering visualization plot has been generated."
return {"file_path": filename, "summary": summary}
else:
return {"file_path": None, "summary": summary}
def hierarchical_cluster(self, n_clusters: int = 3, linkage: str = 'ward', visualize: bool = True):
"""
Applies Hierarchical clustering to the data.
Parameters:
- n_clusters (int): The number of clusters to find.
- linkage (str): Linkage criterion. Supported options are 'ward', 'complete', 'average', 'single'.
- visualize (bool): Whether to visualize the clustering results.
Returns:
- dict: Contains a notification that the plot has been generated and a short summary.
"""
self._scale_features()
hierarchical = AgglomerativeClustering(n_clusters=n_clusters, linkage=linkage)
self.df['cluster_hierarchical'] = hierarchical.fit_predict(self.scaled_features)
summary = f"Hierarchical clustering performed with {n_clusters} clusters and '{linkage}' linkage."
if visualize:
filename = self.plot_clusters('cluster_hierarchical', title='Hierarchical Clustering Results')
summary += " A clustering visualization plot has been generated."
return {"file_path": filename, "summary": summary}
else:
return {"file_path": None, "summary": summary}
def plot_clusters(self, cluster_column: str, title: str = 'Clustering Results', highlight_ids: list = None):
"""
Plots a scatter plot of the clustering results and saves it as a file.
Parameters:
- cluster_column (str): The column name containing cluster labels.
- title (str): The title of the plot.
- highlight_ids (list or None): List of customer IDs to highlight in the plot.
Returns:
- str: The filepath of the saved plot.
"""
plt.figure(figsize=(10, 7))
unique_clusters = self.df[cluster_column].unique()
palette = sns.color_palette('viridis', n_colors=len(unique_clusters))
sns.scatterplot(
data=self.df,
x=self.feature_columns[0],
y=self.feature_columns[1],
hue=cluster_column,
palette=palette,
s=50,
alpha=0.6,
edgecolor='k'
)
# Highlight specific customers if provided
if highlight_ids is not None:
highlight_data = self.df[self.df[self.customer_id_column].isin(highlight_ids)]
sns.scatterplot(
data=highlight_data,
x=self.feature_columns[0],
y=self.feature_columns[1],
color='red',
s=200,
marker='X',
label='Highlighted Customers',
edgecolor='k'
)
plt.title(title)
plt.xlabel(self.feature_columns[0].replace('_', ' ').title())
plt.ylabel(self.feature_columns[1].replace('_', ' ').title())
plt.legend(title='Cluster', bbox_to_anchor=(1.05, 1), loc='upper left')
plt.grid(True)
# Generate filename with timestamp
timestamp = get_timestamp()
filename = f"plots/{title.replace(' ', '_').lower()}_{timestamp}.png"
plt.tight_layout()
plt.savefig(filename)
plt.close()
return filename
# ========================= Correlation Analysis =========================
class CorrelationAnalyzer:
"""
A class for analyzing correlations between columns in a DataFrame.
Supports Pearson, Spearman, and Kendall correlation coefficients.
"""
def __init__(self, df: pd.DataFrame):
"""
Initializes the CorrelationAnalyzer with a DataFrame.
Parameters:
- df (pd.DataFrame): DataFrame containing the data to analyze.
"""
self.df = df.copy()
self.pearson_corr = None
self.spearman_corr = None
self.kendall_corr = None
def calculate_pearson(self):
"""
Calculates the Pearson correlation matrix.
Returns:
- dict: Contains the Pearson correlation matrix and a description.
"""
self.pearson_corr = self.df.corr(method='pearson')
description = "Pearson correlation matrix calculated."
return {"correlation_matrix": self.pearson_corr, "description": description}
def calculate_spearman(self):
"""
Calculates the Spearman correlation matrix.
Returns:
- dict: Contains the Spearman correlation matrix and a description.
"""
self.spearman_corr = self.df.corr(method='spearman')
description = "Spearman correlation matrix calculated."
return {"correlation_matrix": self.spearman_corr, "description": description}
def calculate_kendall(self):
"""
Calculates the Kendall correlation matrix.
Returns:
- dict: Contains the Kendall correlation matrix and a description.
"""
self.kendall_corr = self.df.corr(method='kendall')
description = "Kendall correlation matrix calculated."
return {"correlation_matrix": self.kendall_corr, "description": description}
def visualize_correlation(self, method: str = 'pearson', figsize: tuple = (10, 8), annot: bool = True, cmap: str = 'coolwarm'):
"""
Visualizes the correlation matrix using a heatmap and saves it as a file.
Parameters:
- method (str): The correlation method to visualize ('pearson', 'spearman', 'kendall').
- figsize (tuple): The size of the figure.
- annot (bool): Whether to annotate the heatmap with correlation coefficients.
- cmap (str): The color map to use for the heatmap.
Returns:
- dict: Contains a notification that the plot has been generated and a short summary.
"""
if method == 'pearson':
if self.pearson_corr is None:
self.calculate_pearson()
corr = self.pearson_corr
title = 'Pearson Correlation Matrix'
elif method == 'spearman':
if self.spearman_corr is None:
self.calculate_spearman()
corr = self.spearman_corr
title = 'Spearman Correlation Matrix'
elif method == 'kendall':
if self.kendall_corr is None:
self.calculate_kendall()
corr = self.kendall_corr
title = 'Kendall Correlation Matrix'
else:
raise ValueError("Invalid method. Choose 'pearson', 'spearman', or 'kendall'.")
plt.figure(figsize=figsize)
sns.heatmap(corr, annot=annot, fmt=".2f", cmap=cmap, linewidths=0.5)
plt.title(title)
# Generate filename with timestamp
timestamp = get_timestamp()
filename = f"plots/{title.replace(' ', '_').lower()}_{timestamp}.png"
plt.tight_layout()
plt.savefig(filename)
plt.close()
# Find the highest correlation pair (excluding self-correlation)
corr_values = corr.abs().where(~np.eye(corr.shape[0], dtype=bool))
max_corr = corr_values.unstack().dropna().sort_values(ascending=False).max()
max_pair = corr_values.unstack().idxmax()
summary = f"Highest {method.capitalize()} correlation is between {max_pair[0]} and {max_pair[1]} with a coefficient of {corr.loc[max_pair[0], max_pair[1]]:.2f}."
summary += " A correlation heatmap visualization plot has been generated."
return {"file_path": filename, "summary": summary}
def pairwise_correlation(self, method: str = 'pearson') -> pd.DataFrame:
"""
Computes pairwise correlation coefficients and p-values between variables.
Parameters:
- method (str): The method to use for correlation ('pearson', 'spearman', 'kendall').
Returns:
- pd.DataFrame: A DataFrame containing pairs of variables, their correlation coefficients, and p-values.
"""
cols = self.df.columns
results = []
for i in range(len(cols)):
for j in range(i+1, len(cols)):
var1 = cols[i]
var2 = cols[j]
if method == 'pearson':
corr, p = pearsonr(self.df[var1], self.df[var2])
elif method == 'spearman':
corr, p = spearmanr(self.df[var1], self.df[var2])
elif method == 'kendall':
corr, p = kendalltau(self.df[var1], self.df[var2])
else:
raise ValueError("Invalid method. Choose 'pearson', 'spearman', or 'kendall'.")
results.append({'Variable 1': var1, 'Variable 2': var2, 'Correlation': corr, 'P-value': p})
return pd.DataFrame(results)
def plot_pairwise_relationships(self, kind: str = 'scatter', hue: str = None, figsize: tuple = (12, 10)):
"""
Plots pairwise relationships between variables using Seaborn's pairplot and saves it as a file.
Parameters:
- kind (str): The kind of plot to use ('scatter', 'reg', 'kde', 'hist').
- hue (str): The variable name to use for color encoding.
- figsize (tuple): The size of the figure.
Returns:
- dict: Contains a notification that the plot has been generated and a short summary.
"""
sns.pairplot(self.df, kind=kind, hue=hue, diag_kind='kde', corner=True)
title = 'Pairwise Relationships'
plt.suptitle(title, y=1.02)
# Generate filename with timestamp
timestamp = get_timestamp()
filename = f"plots/{title.replace(' ', '_').lower()}_{timestamp}.png"
plt.tight_layout()
plt.savefig(filename)
plt.close()
summary = "A pairwise relationships plot has been generated."
return {"file_path": filename, "summary": summary}
# ========================= Regression Analysis =========================
class TimeSeriesDataset(Dataset):
"""
Custom Dataset for Time Series data to be used with PyTorch DataLoader.
"""
def __init__(self, sequences, targets):
self.sequences = sequences
self.targets = targets
def __len__(self):
return len(self.sequences)
def __getitem__(self, idx):
return self.sequences[idx], self.targets[idx]
class TransformerRegressor(nn.Module):
"""
Transformer-based model for regression tasks.
"""
def __init__(self, input_size: int = 1, d_model: int = 128, nhead: int = 8, num_encoder_layers: int = 4, dim_feedforward: int = 256, dropout: float = 0.1, max_seq_length: int = 5000):
super(TransformerRegressor, self).__init__()
self.model_type = 'Transformer'
self.d_model = d_model
self.input_linear = nn.Linear(input_size, d_model)
self.pos_encoder = PositionalEncoding(d_model, max_len=max_seq_length)
encoder_layers = nn.TransformerEncoderLayer(d_model, nhead, dim_feedforward, dropout)
self.transformer_encoder = nn.TransformerEncoder(encoder_layers, num_encoder_layers)
self.decoder = nn.Linear(d_model, 1)
self.init_weights()
def init_weights(self):
"""
Initialize weights for the linear layers.
"""
initrange = 0.1
self.input_linear.weight.data.uniform_(-initrange, initrange)
self.input_linear.bias.data.zero_()
self.decoder.weight.data.uniform_(-initrange, initrange)
self.decoder.bias.data.zero_()
def forward(self, src):
"""
Forward pass for the Transformer model.
"""
# src shape: (batch_size, sequence_length, input_size)
src = self.input_linear(src) * np.sqrt(self.d_model) # (batch_size, sequence_length, d_model)
src = self.pos_encoder(src) # (batch_size, sequence_length, d_model)
# Transformer expects input of shape (sequence_length, batch_size, d_model)
src = src.permute(1, 0, 2) # (sequence_length, batch_size, d_model)
output = self.transformer_encoder(src) # (sequence_length, batch_size, d_model)
output = output.permute(1, 0, 2) # (batch_size, sequence_length, d_model)
output = self.decoder(output) # (batch_size, sequence_length, 1)
# Select the last time step's output
output = output[:, -1, :] # (batch_size, 1)
return output
class PositionalEncoding(nn.Module):
"""
Positional Encoding module injects information about the relative or absolute position
of the tokens in the sequence.
"""
def __init__(self, d_model: int, max_len: int = 5000):
super(PositionalEncoding, self).__init__()
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-np.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0) # Shape: (1, max_len, d_model)
self.register_buffer('pe', pe)
def forward(self, x):
# x shape: (batch_size, sequence_length, d_model)
x = x + self.pe[:, :x.size(1), :]
return x
class RegressionAnalysis:
def __init__(self, data: pd.DataFrame, target: str):
"""
Initializes the regression analysis class.
Parameters:
- data (pd.DataFrame): DataFrame containing all features and the target variable.
- target (str): The target variable column name.
"""
self.data = data.copy()
self.target = target
self.X = None
self.y = None
self.models = {}
self.results = {}
self.n_features = 0 # To track the number of predictors
def set_features(self, feature_columns: list):
"""
Sets the predictor and target variables.
Parameters:
- feature_columns (list of str): List of predictor variable column names.
"""
self.X = self.data[feature_columns]
self.y = self.data[self.target]
self.n_features = len(feature_columns)
description = f"Selected features: {feature_columns}\nTarget: {self.target}"
summary = f"Features {feature_columns} selected with target '{self.target}'."
return {"description": description, "summary": summary}
def train_test_split_data(self, test_size: float = 0.2, random_state: int = 42):
"""
Splits the data into training and testing sets.
Parameters:
- test_size (float): Proportion of the dataset to include in the test split.
- random_state (int): Seed used by the random number generator.
"""
self.X_train, self.X_test, self.y_train, self.y_test = train_test_split(
self.X, self.y, test_size=test_size, random_state=random_state
)
description = f"Training samples: {self.X_train.shape[0]}\nTesting samples: {self.X_test.shape[0]}"
summary = f"Data split into {self.X_train.shape[0]} training samples and {self.X_test.shape[0]} testing samples."
return {"description": description, "summary": summary}
def train_ridge(self, alpha: float = 1.0):
model = Ridge(alpha=alpha)
model.fit(self.X_train, self.y_train)
self.models['Ridge'] = model
coefficients = model.coef_
intercept = model.intercept_
coef_dict = dict(zip(self.X.columns, coefficients))
summary = f"Ridge regression model trained with alpha={alpha}."
summary += " A regression visualization plot has been generated."
# Feature Importance Plot
filename = self.plot_feature_importance('Ridge', coef_dict)
return {"file_path": filename, "summary": summary}
def train_lasso(self, alpha: float = 0.1):
model = Lasso(alpha=alpha)
model.fit(self.X_train, self.y_train)
self.models['Lasso'] = model
coefficients = model.coef_
intercept = model.intercept_
coef_dict = dict(zip(self.X.columns, coefficients))
summary = f"Lasso regression model trained with alpha={alpha}."
summary += " A regression visualization plot has been generated."
# Feature Importance Plot
filename = self.plot_feature_importance('Lasso', coef_dict)
return {"file_path": filename, "summary": summary}
def train_svm(self, C: float = 1.0, kernel: str = 'rbf'):
model = SVR(C=C, kernel=kernel)
model.fit(self.X_train, self.y_train)
self.models['SVM'] = model
summary = f"SVM regression model trained with C={C} and kernel='{kernel}'."
summary += " A regression visualization plot has been generated."
# Actual vs Predicted Plot
filename = self.plot_actual_vs_predicted('SVM')
return {"file_path": filename, "summary": summary}
def train_random_forest(self, n_estimators: int = 100, random_state: int = 42):
model = RandomForestRegressor(n_estimators=n_estimators, random_state=random_state)
model.fit(self.X_train, self.y_train)
self.models['RandomForest'] = model
summary = f"Random Forest regression model trained with {n_estimators} estimators and random_state={random_state}."
summary += " A feature importance visualization plot has been generated."
# Feature Importance Plot
filename = self.plot_feature_importance('RandomForest', model.feature_importances_)
return {"file_path": filename, "summary": summary}
def train_transformer(self, steps: int = 6, epochs: int = 200, batch_size: int = 16, learning_rate: float = 1e-3, window_size: int = 24, patience: int = 20):
"""
Trains a Transformer-based regression model.
Parameters:
- steps (int): Number of future observations to predict.
- epochs (int): Number of training epochs.
- batch_size (int): Batch size for training.
- learning_rate (float): Learning rate for the optimizer.
- window_size (int): Number of past observations to use for each prediction.
- patience (int): Number of epochs to wait for improvement before stopping.
"""
class TransformerDatasetLocal(Dataset):
def __init__(self, sequences, targets):
self.sequences = sequences
self.targets = targets
def __len__(self):
return len(self.sequences)
def __getitem__(self, idx):
return self.sequences[idx], self.targets[idx]
# Scaling the data
scaler_X = StandardScaler()
scaler_y = StandardScaler()
X_scaled = scaler_X.fit_transform(self.X_train)
y_scaled = scaler_y.fit_transform(self.y_train.values.reshape(-1, 1)).flatten()
# Creating sequences
sequences = []
targets = []
for i in range(len(X_scaled) - window_size):
sequences.append(X_scaled[i:i+window_size])
targets.append(y_scaled[i+window_size])
sequences = np.array(sequences)
targets = np.array(targets)
# Convert to tensors
sequences = torch.tensor(sequences, dtype=torch.float32).unsqueeze(-1) # Shape: (num_samples, window_size, 1)
targets = torch.tensor(targets, dtype=torch.float32).unsqueeze(-1) # Shape: (num_samples, 1)
# Create dataset and dataloader
dataset = TransformerDatasetLocal(sequences, targets)
train_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True) # Increased batch size for better training
# Initialize model, loss, and optimizer
input_size = self.X_train.shape[1]
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = TransformerRegressor(input_size=input_size).to(device)
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=10, verbose=False)
# Early stopping parameters
best_loss = float('inf')
epochs_no_improve = 0
early_stop = False
# Training loop with early stopping
model.train()
for epoch in tqdm(range(epochs), desc="Training Transformer", unit="epoch"):
epoch_losses = []
for batch_X, batch_y in train_loader:
batch_X = batch_X.to(device) # Shape: (batch_size, window_size, input_size)
batch_y = batch_y.to(device) # Shape: (batch_size, 1)
optimizer.zero_grad()
output = model(batch_X) # Shape: (batch_size, 1)
loss = criterion(output, batch_y)
loss.backward()
optimizer.step()
epoch_losses.append(loss.item())
avg_loss = np.mean(epoch_losses)
scheduler.step(avg_loss)
# Early stopping check
if avg_loss < best_loss:
best_loss = avg_loss
epochs_no_improve = 0
# Save the best model
best_model_state = model.state_dict()
else:
epochs_no_improve += 1
if epochs_no_improve >= patience:
early_stop = True
break
# Early stopping flag
if early_stop:
break
# Load the best model
model.load_state_dict(best_model_state)
# Forecasting
model.eval()
sales = self.data[self.target].values
sales_scaled = scaler_X.transform(self.X_train).flatten()
forecast_scaled = []
# Initialize the current sequence with the last window_size points
current_seq = torch.tensor(sales_scaled[-window_size:], dtype=torch.float32).unsqueeze(0).unsqueeze(-1).to(device) # Shape: (1, window_size, 1)
for _ in range(steps):
with torch.no_grad():
pred_scaled = model(current_seq).cpu().numpy().flatten()[0]
forecast_scaled.append(pred_scaled)
# Update the current sequence by removing the first element and adding the new prediction
new_input = torch.tensor([[pred_scaled]], dtype=torch.float32).unsqueeze(-1).to(device) # Shape: (1, 1, 1)
current_seq = torch.cat((current_seq[:,1:,:], new_input), dim=1) # Shape: (1, window_size, 1)
# Inverse scaling
forecast = scaler_y.inverse_transform(np.array(forecast_scaled).reshape(-1,1)).flatten()
# Generate future dates based on frequency
last_date = self.data.index[-1]
freq_offset = pd.tseries.frequencies.to_offset(self.frequency)
forecast_dates = [last_date + (freq_offset * (i+1)) for i in range(steps)]
forecast_series = pd.Series(forecast, index=forecast_dates)
self.predictions = forecast_series
self.future_dates = forecast_dates
self.models['Transformer'] = {
'model': model,
'scaler_X': scaler_X,
'scaler_y': scaler_y,
'window_size': window_size
}
# Plot and save the forecast
plot_info = self.plot_forecast(forecast_series, 'Transformer')
# Prepare summary with forecasted values
forecast_values = forecast_series.to_dict()
forecast_summary = "Forecasted values:\n"
for date, value in forecast_values.items():
forecast_summary += f"[{date.date()}: {value:.2f}]\n"
forecast_summary += "A time series forecast visualization plot has been generated."
return {"file_path": plot_info["file_path"], "summary": forecast_summary}
def evaluate_models(self):
"""
Evaluates all trained models and stores the results.
Returns:
- dict: Contains evaluation metrics and a summary.
"""
evaluation_results = {}
summaries = []
for name, model in self.models.items():
if name != 'Transformer':
predictions = model.predict(self.X_test)
# Extract model parameters if applicable
if hasattr(model, 'coef_') and hasattr(model, 'intercept_'):
coefficients = model.coef_
intercept = model.intercept_
coef_dict = dict(zip(self.X.columns, coefficients))
evaluation_results[name] = {
'MSE': mean_squared_error(self.y_test, predictions),
'R2': r2_score(self.y_test, predictions),
'Intercept': intercept,
'Coefficients': coef_dict
}
summary_str = f"{name} Model - Intercept: {intercept:.4f}, Coefficients: {coef_dict}"
else:
evaluation_results[name] = {
'MSE': mean_squared_error(self.y_test, predictions),
'R2': r2_score(self.y_test, predictions)
}
summary_str = f"{name} Model - MSE: {evaluation_results[name]['MSE']:.4f}, R2: {evaluation_results[name]['R2']:.4f}"
else:
# For Transformer, perform scaling and forecasting
transformer_info = model
scaler_X = transformer_info['scaler_X']
scaler_y = transformer_info['scaler_y']
window_size = transformer_info['window_size']
transformer_model = transformer_info['model']
transformer_model.eval()
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# Prepare test data
X_test_scaled = scaler_X.transform(self.X_test)
y_test_scaled = scaler_y.transform(self.y_test.values.reshape(-1, 1)).flatten()
sequences = []
targets = []
for i in range(len(X_test_scaled) - window_size):
sequences.append(X_test_scaled[i:i+window_size])
targets.append(y_test_scaled[i+window_size])
sequences = np.array(sequences)
targets = np.array(targets)
sequences = torch.tensor(sequences, dtype=torch.float32).to(device)
targets = torch.tensor(targets, dtype=torch.float32).to(device)
with torch.no_grad():
predictions_scaled = transformer_model(sequences).cpu().numpy().flatten()
predictions = scaler_y.inverse_transform(predictions_scaled.reshape(-1,1)).flatten()
mse = mean_squared_error(self.y_test.iloc[window_size:], predictions)
r2 = r2_score(self.y_test.iloc[window_size:], predictions)
evaluation_results[name] = {'MSE': mse, 'R2': r2}
summary_str = f"{name} Model - MSE: {mse:.4f}, R2: {r2:.4f}"
evaluation_results[name]['summary'] = summary_str
summaries.append(summary_str)
self.results = evaluation_results
summary = "Model evaluations completed. " + "; ".join(summaries)
return {"file_path": None, "summary": summary}
def plot_feature_importance(self, model_name: str, importances):
"""
Plots feature importances for models that support it (e.g., Random Forest, Ridge, Lasso).
Parameters:
- model_name (str): Name of the model.
- importances (dict or array-like): Feature importances or coefficients.
Returns:
- str: The filepath of the saved plot.
"""
plt.figure(figsize=(10, 6))
features = self.X.columns
if model_name in ['Ridge', 'Lasso']:
# Ensure coefficients are ordered according to features
if isinstance(importances, dict):
coef = [importances[feature] for feature in features]
else:
coef = importances
sns.barplot(x=features, y=coef, palette='viridis')
plt.axhline(0, color='red', linestyle='--')
plt.title(f'{model_name} Coefficients')
plt.xlabel('Features')
plt.ylabel('Coefficient Value')
elif model_name == 'RandomForest':
# For Random Forest, importances are all positive
importances = importances
sns.barplot(x=features, y=importances, palette='viridis')
plt.title('Random Forest Feature Importances')
plt.xlabel('Features')
plt.ylabel('Importance')
else:
raise ValueError("Feature importance plotting not supported for this model.")
plt.xticks(rotation=45)
plt.tight_layout()
filename = f"plots/{model_name.lower()}_feature_importances_{get_timestamp()}.png"
plt.savefig(filename)
plt.close()
return filename
def plot_actual_vs_predicted(self, model_name: str):
"""
Plots actual vs predicted values for regression models and saves it as a file.
Parameters:
- model_name (str): Name of the model.
Returns:
- str: The filepath of the saved plot.
"""
predictions = self.models[model_name].predict(self.X_test)
plt.figure(figsize=(10, 6))
sns.scatterplot(x=self.y_test, y=predictions, alpha=0.6)
sns.lineplot(x=self.y_test, y=self.y_test, color='red') # Perfect prediction line
plt.title(f'Actual vs Predicted Sales ({model_name})')
plt.xlabel('Actual Sales')
plt.ylabel('Predicted Sales')
plt.grid(True)
plt.tight_layout()
filename = f"plots/{model_name.lower()}_actual_vs_predicted_{get_timestamp()}.png"
plt.savefig(filename)
plt.close()
return filename
def plot_results(self):
"""
Plots actual vs predicted values for univariate models with continuous regression curves.
Labels the true and predicted values of the test set.
Saves the plots as files.
Returns:
- list of dict: Each dictionary contains the file path and summary of the saved plot.
"""
# Ensure the plots directory exists
os.makedirs('plots', exist_ok=True)
plot_results = []
for name, model in self.models.items():
if name != 'Transformer':
# For non-Transformer models, get predictions for training and testing sets
predictions_train = model.predict(self.X_train)
predictions_test = model.predict(self.X_test)
# Extract model parameters if applicable
if hasattr(model, 'coef_') and hasattr(model, 'intercept_'):
intercept = model.intercept_
coefficients = model.coef_
coef_dict = dict(zip(self.X.columns, coefficients))
else:
intercept = None
coef_dict = None
else:
# For Transformer, perform scaling and forecasting
transformer_info = model
scaler_X = transformer_info['scaler_X']
scaler_y = transformer_info['scaler_y']
window_size = transformer_info['window_size']
transformer_model = transformer_info['model']
transformer_model.eval()
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# Prepare training data
X_train_scaled = scaler_X.transform(self.X_train)
y_train_scaled = scaler_y.transform(self.y_train.values.reshape(-1, 1)).flatten()
sequences_train = []
targets_train = []
for i in range(len(X_train_scaled) - window_size):
sequences_train.append(X_train_scaled[i:i+window_size])
targets_train.append(y_train_scaled[i+window_size])
sequences_train = np.array(sequences_train)
targets_train = np.array(targets_train)
sequences_train = torch.tensor(sequences_train, dtype=torch.float32).to(device)
targets_train = torch.tensor(targets_train, dtype=torch.float32).to(device)
with torch.no_grad():
predictions_train_scaled = transformer_model(sequences_train).cpu().numpy().flatten()
predictions_train = scaler_y.inverse_transform(predictions_train_scaled.reshape(-1,1)).flatten()
# Prepare testing data
X_test_scaled = scaler_X.transform(self.X_test)
y_test_scaled = scaler_y.transform(self.y_test.values.reshape(-1, 1)).flatten()
sequences_test = []
targets_test = []
for i in range(len(X_test_scaled) - window_size):
sequences_test.append(X_test_scaled[i:i+window_size])
targets_test.append(y_test_scaled[i+window_size])
sequences_test = np.array(sequences_test)
targets_test = np.array(targets_test)
sequences_test = torch.tensor(sequences_test, dtype=torch.float32).to(device)
targets_test = torch.tensor(targets_test, dtype=torch.float32).to(device)
with torch.no_grad():
predictions_test_scaled = transformer_model(sequences_test).cpu().numpy().flatten()
predictions_test = scaler_y.inverse_transform(predictions_test_scaled.reshape(-1,1)).flatten()
# Determine if the model is univariate
if self.n_features == 1:
# Univariate regression: Plot training and testing data points and continuous regression curve
plt.figure(figsize=(12, 8))
predictor = self.X.columns[0]
# Plot training data points
plt.scatter(self.X_train[predictor], self.y_train, color='blue', label='Training Data', alpha=0.6)
# Plot testing data points (true values)
plt.scatter(self.X_test[predictor], self.y_test, color='red', marker='x', label='Test Data (True)', alpha=0.6)
# Plot testing data points (predicted values)
plt.scatter(self.X_test[predictor], predictions_test, color='green', marker='o', label='Test Data (Predicted)', alpha=0.6)
# Generate a smooth range of feature values for plotting the regression curve
feature_min = self.X[predictor].min()
feature_max = self.X[predictor].max()
feature_range = np.linspace(feature_min, feature_max, 500).reshape(-1, 1)
if name != 'Transformer':
# For non-Transformer models, predict on the feature range
predictions_curve = model.predict(feature_range)
else:
# For Transformer models, generating a continuous curve is more complex
# Here's a simplified approach assuming window_size=1 for demonstration
# Adjust according to your actual window_size and Transformer architecture
if transformer_info['window_size'] == 1:
X_curve_scaled = transformer_info['scaler_X'].transform(feature_range)
sequences_curve = X_curve_scaled.reshape(-1, 1, self.n_features)
sequences_curve = torch.tensor(sequences_curve, dtype=torch.float32).to(device)
with torch.no_grad():
predictions_curve_scaled = transformer_model(sequences_curve).cpu().numpy().flatten()
predictions_curve = transformer_info['scaler_y'].inverse_transform(predictions_curve_scaled.reshape(-1,1)).flatten()
else:
# If window_size > 1, more sophisticated handling is required
# Here, we'll skip plotting for Transformer with window_size > 1
predictions_curve = np.nan * np.ones(feature_range.shape[0])
print(f"Continuous curve plotting for Transformer with window_size={transformer_info['window_size']} is not implemented.")
if not np.isnan(predictions_curve).all():
# Plot the continuous regression curve
plt.plot(feature_range, predictions_curve, color='purple', label='Regression Curve', linewidth=2)
plt.xlabel(predictor.replace('_', ' ').title())
plt.ylabel(self.target.replace('_', ' ').title())
plt.title(f"Regression Analysis: {self.target} vs {predictor} ({name})")
plt.legend()
plt.grid(True)
# Generate filename with timestamp
timestamp = get_timestamp()
filename = f"plots/{self.target}_vs_{predictor}_{name.lower()}_{timestamp}.png"
plt.tight_layout()
plt.savefig(filename)
plt.close()
# Prepare summary
if name != 'Transformer':
summary = f"{name} Model - Intercept: {intercept:.4f}, Coefficients: {coef_dict}"
else:
summary = f"{name} Model - No intercept or coefficients available."
summary += " A regression visualization plot has been generated."
plot_results.append({"file_path": filename, "summary": summary})
else:
# Multivariate regression: Generate Actual vs Predicted Plot
if name != 'Transformer':
predictions = model.predict(self.X_test)
else:
predictions = predictions_test
plt.figure(figsize=(10, 6))
sns.scatterplot(x=self.y_test, y=predictions, alpha=0.6)
sns.lineplot(x=self.y_test, y=self.y_test, color='red') # Perfect prediction line
plt.title(f'Actual vs Predicted Sales ({name})')
plt.xlabel('Actual Sales')
plt.ylabel('Predicted Sales')
plt.grid(True)
plt.tight_layout()
filename = f"plots/{name.lower()}_actual_vs_predicted_{get_timestamp()}.png"
plt.savefig(filename)
plt.close()
# Prepare summary
if name != 'Transformer':
summary = f"{name} Model - Coefficients: {coef_dict}"
else: