TayyabManan commited on
Commit
4ac1910
·
1 Parent(s): 080d992

Add stacking ensemble, threshold tuning, and evaluation notebooks

Browse files
notebook/{eda.ipynb → 1_Exploratory_Data_Analysis.ipynb} RENAMED
@@ -167,6 +167,7 @@
167
  }
168
  ],
169
  "source": [
 
170
  "import pandas as pd\n",
171
  "import numpy as np\n",
172
  "import matplotlib.pyplot as plt\n",
@@ -177,6 +178,8 @@
177
  "plt.rcParams[\"figure.figsize\"] = (12, 5)\n",
178
  "plt.rcParams[\"figure.dpi\"] = 120\n",
179
  "\n",
 
 
180
  "CSV_PATH = \"../EasyVisa.csv\"\n",
181
  "df = pd.read_csv(CSV_PATH)\n",
182
  "print(f\"Shape: {df.shape}\")\n",
@@ -714,6 +717,7 @@
714
  "axes[1].set_title(\"Case Status — Proportion\", fontweight=\"bold\")\n",
715
  "\n",
716
  "plt.tight_layout()\n",
 
717
  "plt.show()\n",
718
  "\n",
719
  "print(f\"\\nImbalance ratio: {counts.iloc[0] / counts.iloc[1]:.2f}:1 (Certified:Denied)\")\n",
@@ -837,6 +841,7 @@
837
  " f\"{val:.1f}%\", va=\"center\", fontsize=9)\n",
838
  "\n",
839
  " plt.tight_layout()\n",
 
840
  " plt.show()"
841
  ]
842
  },
@@ -856,7 +861,7 @@
856
  "name": "stderr",
857
  "output_type": "stream",
858
  "text": [
859
- "C:\\Users\\haris\\AppData\\Local\\Temp\\ipykernel_3800\\2242569204.py:12: FutureWarning: \n",
860
  "\n",
861
  "Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.\n",
862
  "\n",
@@ -890,7 +895,7 @@
890
  "name": "stderr",
891
  "output_type": "stream",
892
  "text": [
893
- "C:\\Users\\haris\\AppData\\Local\\Temp\\ipykernel_3800\\2242569204.py:12: FutureWarning: \n",
894
  "\n",
895
  "Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.\n",
896
  "\n",
@@ -929,7 +934,7 @@
929
  "name": "stderr",
930
  "output_type": "stream",
931
  "text": [
932
- "C:\\Users\\haris\\AppData\\Local\\Temp\\ipykernel_3800\\2242569204.py:12: FutureWarning: \n",
933
  "\n",
934
  "Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.\n",
935
  "\n",
@@ -985,6 +990,7 @@
985
  " axes[2].set_ylabel(\"Density\")\n",
986
  "\n",
987
  " plt.tight_layout()\n",
 
988
  " plt.show()\n",
989
  "\n",
990
  " # Stats\n",
@@ -1053,6 +1059,7 @@
1053
  " cbar_kws={\"shrink\": 0.8})\n",
1054
  "ax.set_title(\"Feature Correlation Matrix\", fontweight=\"bold\", fontsize=13)\n",
1055
  "plt.tight_layout()\n",
 
1056
  "plt.show()\n",
1057
  "\n",
1058
  "print(\"\\nCorrelation with Denied (case_status_num):\")\n",
@@ -1077,7 +1084,7 @@
1077
  "name": "stderr",
1078
  "output_type": "stream",
1079
  "text": [
1080
- "C:\\Users\\haris\\AppData\\Local\\Temp\\ipykernel_3800\\1804773271.py:17: FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning.\n",
1081
  " cert_by_wage = df.groupby(\"wage_quartile\")[\"case_status\"].apply(lambda x: (x == \"Certified\").mean() * 100)\n"
1082
  ]
1083
  },
@@ -1119,6 +1126,7 @@
1119
  " ha=\"center\", fontsize=10, fontweight=\"bold\")\n",
1120
  "\n",
1121
  "plt.tight_layout()\n",
 
1122
  "plt.show()\n",
1123
  "\n",
1124
  "df.drop(columns=[\"annual_wage\", \"wage_quartile\"], inplace=True)"
@@ -1186,6 +1194,7 @@
1186
  "ax.set_title(\"Top 15 Feature Importances (Random Forest)\", fontweight=\"bold\")\n",
1187
  "ax.set_xlabel(\"Importance\")\n",
1188
  "plt.tight_layout()\n",
 
1189
  "plt.show()"
1190
  ]
1191
  },
@@ -1195,7 +1204,7 @@
1195
  "source": [
1196
  "## 8. Key Takeaways\n",
1197
  "\n",
1198
- "1. **Class imbalance** (66.8% Certified / 33.2% Denied) requires resampling (SMOTEENN) and F1 scoring accuracy alone is misleading.\n",
1199
  "\n",
1200
  "2. **Education** has the clearest ordinal relationship with approval. Doctorate > Master's > Bachelor's > High School.\n",
1201
  "\n",
 
167
  }
168
  ],
169
  "source": [
170
+ "import os\n",
171
  "import pandas as pd\n",
172
  "import numpy as np\n",
173
  "import matplotlib.pyplot as plt\n",
 
178
  "plt.rcParams[\"figure.figsize\"] = (12, 5)\n",
179
  "plt.rcParams[\"figure.dpi\"] = 120\n",
180
  "\n",
181
+ "os.makedirs(\"../figures\", exist_ok=True)\n",
182
+ "\n",
183
  "CSV_PATH = \"../EasyVisa.csv\"\n",
184
  "df = pd.read_csv(CSV_PATH)\n",
185
  "print(f\"Shape: {df.shape}\")\n",
 
717
  "axes[1].set_title(\"Case Status — Proportion\", fontweight=\"bold\")\n",
718
  "\n",
719
  "plt.tight_layout()\n",
720
+ "plt.savefig(\"../figures/fig1_class_distribution.png\", dpi=150, bbox_inches=\"tight\")\n",
721
  "plt.show()\n",
722
  "\n",
723
  "print(f\"\\nImbalance ratio: {counts.iloc[0] / counts.iloc[1]:.2f}:1 (Certified:Denied)\")\n",
 
841
  " f\"{val:.1f}%\", va=\"center\", fontsize=9)\n",
842
  "\n",
843
  " plt.tight_layout()\n",
844
+ " plt.savefig(f\"../figures/fig2_{feat}.png\", dpi=150, bbox_inches=\"tight\")\n",
845
  " plt.show()"
846
  ]
847
  },
 
861
  "name": "stderr",
862
  "output_type": "stream",
863
  "text": [
864
+ "C:\\Users\\haris\\AppData\\Local\\Temp\\ipykernel_35396\\1005620750.py:12: FutureWarning: \n",
865
  "\n",
866
  "Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.\n",
867
  "\n",
 
895
  "name": "stderr",
896
  "output_type": "stream",
897
  "text": [
898
+ "C:\\Users\\haris\\AppData\\Local\\Temp\\ipykernel_35396\\1005620750.py:12: FutureWarning: \n",
899
  "\n",
900
  "Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.\n",
901
  "\n",
 
934
  "name": "stderr",
935
  "output_type": "stream",
936
  "text": [
937
+ "C:\\Users\\haris\\AppData\\Local\\Temp\\ipykernel_35396\\1005620750.py:12: FutureWarning: \n",
938
  "\n",
939
  "Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.\n",
940
  "\n",
 
990
  " axes[2].set_ylabel(\"Density\")\n",
991
  "\n",
992
  " plt.tight_layout()\n",
993
+ " plt.savefig(f\"../figures/fig3_{feat}.png\", dpi=150, bbox_inches=\"tight\")\n",
994
  " plt.show()\n",
995
  "\n",
996
  " # Stats\n",
 
1059
  " cbar_kws={\"shrink\": 0.8})\n",
1060
  "ax.set_title(\"Feature Correlation Matrix\", fontweight=\"bold\", fontsize=13)\n",
1061
  "plt.tight_layout()\n",
1062
+ "plt.savefig(\"../figures/fig4_correlation_matrix.png\", dpi=150, bbox_inches=\"tight\")\n",
1063
  "plt.show()\n",
1064
  "\n",
1065
  "print(\"\\nCorrelation with Denied (case_status_num):\")\n",
 
1084
  "name": "stderr",
1085
  "output_type": "stream",
1086
  "text": [
1087
+ "C:\\Users\\haris\\AppData\\Local\\Temp\\ipykernel_35396\\3879025697.py:17: FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning.\n",
1088
  " cert_by_wage = df.groupby(\"wage_quartile\")[\"case_status\"].apply(lambda x: (x == \"Certified\").mean() * 100)\n"
1089
  ]
1090
  },
 
1126
  " ha=\"center\", fontsize=10, fontweight=\"bold\")\n",
1127
  "\n",
1128
  "plt.tight_layout()\n",
1129
+ "plt.savefig(\"../figures/fig5_wage_analysis.png\", dpi=150, bbox_inches=\"tight\")\n",
1130
  "plt.show()\n",
1131
  "\n",
1132
  "df.drop(columns=[\"annual_wage\", \"wage_quartile\"], inplace=True)"
 
1194
  "ax.set_title(\"Top 15 Feature Importances (Random Forest)\", fontweight=\"bold\")\n",
1195
  "ax.set_xlabel(\"Importance\")\n",
1196
  "plt.tight_layout()\n",
1197
+ "plt.savefig(\"../figures/fig6_feature_importance.png\", dpi=150, bbox_inches=\"tight\")\n",
1198
  "plt.show()"
1199
  ]
1200
  },
 
1204
  "source": [
1205
  "## 8. Key Takeaways\n",
1206
  "\n",
1207
+ "1. **Class imbalance** (66.8% Certified / 33.2% Denied) is handled via native class weighting (LightGBM, CatBoost) and post-training threshold tuning rather than resampling. Accuracy alone is misleading — we enforce a denied recall constraint (>=60%).\n",
1208
  "\n",
1209
  "2. **Education** has the clearest ordinal relationship with approval. Doctorate > Master's > Bachelor's > High School.\n",
1210
  "\n",
notebook/2_Feature_Engineering_and_Model_Selection.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
notebook/3_Model_Evaluation.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
visa_approval_prediction/components/__init__.py ADDED
File without changes
visa_approval_prediction/components/data_ingestion.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ import pandas as pd
5
+ from sklearn.model_selection import train_test_split
6
+
7
+ from visa_approval_prediction.constants import CURRENT_YEAR, TARGET_COLUMN
8
+ from visa_approval_prediction.entity.config_entity import DataIngestionConfig
9
+ from visa_approval_prediction.entity.artifact_entity import DataIngestionArtifact
10
+ from visa_approval_prediction.exception import visaException
11
+ from visa_approval_prediction.logger import logging
12
+
13
+
14
+ class DataIngestion:
15
+ def __init__(self, config: DataIngestionConfig):
16
+ self.config = config
17
+
18
+ def initiate_data_ingestion(self) -> DataIngestionArtifact:
19
+ logging.info("Starting data ingestion")
20
+ try:
21
+ df = pd.read_csv(self.config.data_source_path)
22
+ logging.info(f"Loaded dataset: {df.shape}")
23
+
24
+ # Feature engineering
25
+ df["company_age"] = CURRENT_YEAR - df["yr_of_estab"]
26
+ df.drop(columns=["case_id", "yr_of_estab"], inplace=True)
27
+
28
+ # Encode target: Certified=0, Denied=1
29
+ df[TARGET_COLUMN] = df[TARGET_COLUMN].map({"Certified": 0, "Denied": 1})
30
+
31
+ # Stratified train/test split
32
+ train_set, test_set = train_test_split(
33
+ df,
34
+ test_size=self.config.split_ratio,
35
+ random_state=42,
36
+ stratify=df[TARGET_COLUMN],
37
+ )
38
+ logging.info(f"Train: {train_set.shape}, Test: {test_set.shape}")
39
+
40
+ # Save splits
41
+ os.makedirs(os.path.dirname(self.config.train_file_path), exist_ok=True)
42
+ train_set.to_csv(self.config.train_file_path, index=False)
43
+ test_set.to_csv(self.config.test_file_path, index=False)
44
+ logging.info(f"Train saved to {self.config.train_file_path}")
45
+ logging.info(f"Test saved to {self.config.test_file_path}")
46
+
47
+ return DataIngestionArtifact(
48
+ train_file_path=self.config.train_file_path,
49
+ test_file_path=self.config.test_file_path,
50
+ )
51
+
52
+ except Exception as e:
53
+ raise visaException(e, sys) from e
visa_approval_prediction/components/data_transformation.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import pickle
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from sklearn.compose import ColumnTransformer
8
+ from sklearn.pipeline import Pipeline as SkPipeline
9
+ from sklearn.preprocessing import (
10
+ OneHotEncoder,
11
+ OrdinalEncoder,
12
+ PowerTransformer,
13
+ StandardScaler,
14
+ )
15
+ from visa_approval_prediction.constants import TARGET_COLUMN
16
+ from visa_approval_prediction.entity.config_entity import DataTransformationConfig
17
+ from visa_approval_prediction.entity.artifact_entity import (
18
+ DataIngestionArtifact,
19
+ DataTransformationArtifact,
20
+ )
21
+ from visa_approval_prediction.exception import visaException
22
+ from visa_approval_prediction.logger import logging
23
+
24
+
25
+ class DataTransformation:
26
+ def __init__(
27
+ self,
28
+ config: DataTransformationConfig,
29
+ ingestion_artifact: DataIngestionArtifact,
30
+ ):
31
+ self.config = config
32
+ self.ingestion_artifact = ingestion_artifact
33
+
34
+ @staticmethod
35
+ def _build_preprocessor() -> ColumnTransformer:
36
+ """Build the ColumnTransformer (must match prediction pipeline expectations)."""
37
+ onehot_cols = ["continent", "unit_of_wage", "region_of_employment"]
38
+ ordinal_cols = [
39
+ "has_job_experience",
40
+ "requires_job_training",
41
+ "full_time_position",
42
+ "education_of_employee",
43
+ ]
44
+ ordinal_categories = [
45
+ ["N", "Y"],
46
+ ["N", "Y"],
47
+ ["N", "Y"],
48
+ ["High School", "Bachelor's", "Master's", "Doctorate"],
49
+ ]
50
+ power_scale_cols = ["no_of_employees", "company_age"]
51
+ scale_only_cols = ["prevailing_wage"]
52
+
53
+ return ColumnTransformer(
54
+ transformers=[
55
+ (
56
+ "onehot",
57
+ OneHotEncoder(handle_unknown="ignore", sparse_output=False),
58
+ onehot_cols,
59
+ ),
60
+ (
61
+ "ordinal",
62
+ OrdinalEncoder(categories=ordinal_categories),
63
+ ordinal_cols,
64
+ ),
65
+ (
66
+ "power_scale",
67
+ SkPipeline(
68
+ [
69
+ ("power", PowerTransformer(method="yeo-johnson")),
70
+ ("scale", StandardScaler()),
71
+ ]
72
+ ),
73
+ power_scale_cols,
74
+ ),
75
+ ("scale", StandardScaler(), scale_only_cols),
76
+ ],
77
+ remainder="drop",
78
+ )
79
+
80
+ def initiate_data_transformation(self) -> DataTransformationArtifact:
81
+ logging.info("Starting data transformation")
82
+ try:
83
+ train_df = pd.read_csv(self.ingestion_artifact.train_file_path)
84
+ test_df = pd.read_csv(self.ingestion_artifact.test_file_path)
85
+
86
+ X_train = train_df.drop(columns=[TARGET_COLUMN])
87
+ y_train = train_df[TARGET_COLUMN]
88
+ X_test = test_df.drop(columns=[TARGET_COLUMN])
89
+ y_test = test_df[TARGET_COLUMN]
90
+
91
+ # Fit preprocessor on training data only (no data leakage)
92
+ preprocessor = self._build_preprocessor()
93
+ logging.info("Fitting preprocessor on training data")
94
+ X_train_transformed = preprocessor.fit_transform(X_train)
95
+ X_test_transformed = preprocessor.transform(X_test)
96
+ logging.info(
97
+ f"Transformed — Train: {X_train_transformed.shape}, "
98
+ f"Test: {X_test_transformed.shape}"
99
+ )
100
+
101
+ # Train on natural distribution (no resampling)
102
+ logging.info(
103
+ f"Training on natural distribution: {X_train_transformed.shape[0]} samples"
104
+ )
105
+
106
+ # Save transformed arrays
107
+ for path in [
108
+ self.config.transformed_train_file_path,
109
+ self.config.preprocessor_object_file_path,
110
+ ]:
111
+ os.makedirs(os.path.dirname(path), exist_ok=True)
112
+
113
+ np.save(self.config.transformed_train_file_path, X_train_transformed)
114
+ np.save(self.config.transformed_test_file_path, X_test_transformed)
115
+ np.save(self.config.transformed_train_target_path, np.array(y_train))
116
+ np.save(self.config.transformed_test_target_path, np.array(y_test))
117
+
118
+ # Save preprocessor
119
+ with open(self.config.preprocessor_object_file_path, "wb") as f:
120
+ pickle.dump(preprocessor, f)
121
+ logging.info(
122
+ f"Preprocessor saved to {self.config.preprocessor_object_file_path}"
123
+ )
124
+
125
+ return DataTransformationArtifact(
126
+ transformed_train_file_path=self.config.transformed_train_file_path,
127
+ transformed_test_file_path=self.config.transformed_test_file_path,
128
+ transformed_train_target_path=self.config.transformed_train_target_path,
129
+ transformed_test_target_path=self.config.transformed_test_target_path,
130
+ preprocessor_object_file_path=self.config.preprocessor_object_file_path,
131
+ )
132
+
133
+ except Exception as e:
134
+ raise visaException(e, sys) from e
visa_approval_prediction/components/data_validation.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ import yaml
5
+ import pandas as pd
6
+ from scipy.stats import ks_2samp
7
+
8
+ from visa_approval_prediction.entity.config_entity import DataValidationConfig
9
+ from visa_approval_prediction.entity.artifact_entity import (
10
+ DataIngestionArtifact,
11
+ DataValidationArtifact,
12
+ )
13
+ from visa_approval_prediction.exception import visaException
14
+ from visa_approval_prediction.logger import logging
15
+
16
+
17
+ class DataValidation:
18
+ def __init__(
19
+ self,
20
+ config: DataValidationConfig,
21
+ ingestion_artifact: DataIngestionArtifact,
22
+ ):
23
+ self.config = config
24
+ self.ingestion_artifact = ingestion_artifact
25
+
26
+ def _read_schema(self) -> dict:
27
+ with open(self.config.schema_file_path, "r") as f:
28
+ return yaml.safe_load(f)
29
+
30
+ def _validate_columns(self, df: pd.DataFrame, schema: dict) -> bool:
31
+ """Check all expected columns are present after feature engineering."""
32
+ expected = set()
33
+ for col_def in schema["columns"]:
34
+ if isinstance(col_def, dict):
35
+ col_name = list(col_def.keys())[0]
36
+ else:
37
+ col_name = col_def
38
+ expected.add(col_name)
39
+
40
+ # Adjust for ingestion-stage feature engineering
41
+ expected.discard("case_id")
42
+ expected.discard("yr_of_estab")
43
+ expected.add("company_age")
44
+
45
+ actual = set(df.columns)
46
+ missing = expected - actual
47
+
48
+ if missing:
49
+ logging.warning(f"Missing columns: {missing}")
50
+ return False
51
+ return True
52
+
53
+ def _detect_drift(
54
+ self, train_df: pd.DataFrame, test_df: pd.DataFrame
55
+ ) -> dict:
56
+ """KS test on numerical columns to detect distribution drift."""
57
+ report = {}
58
+ numerical_cols = train_df.select_dtypes(include=["int64", "float64"]).columns
59
+
60
+ for col in numerical_cols:
61
+ stat, p_value = ks_2samp(train_df[col], test_df[col])
62
+ is_drifted = p_value < 0.05
63
+ report[col] = {
64
+ "ks_statistic": float(round(stat, 4)),
65
+ "p_value": float(round(p_value, 4)),
66
+ "drift_detected": is_drifted,
67
+ }
68
+ if is_drifted:
69
+ logging.warning(f"Drift in '{col}' (p={p_value:.4f})")
70
+
71
+ return report
72
+
73
+ def initiate_data_validation(self) -> DataValidationArtifact:
74
+ logging.info("Starting data validation")
75
+ try:
76
+ train_df = pd.read_csv(self.ingestion_artifact.train_file_path)
77
+ test_df = pd.read_csv(self.ingestion_artifact.test_file_path)
78
+ schema = self._read_schema()
79
+
80
+ # Column validation
81
+ train_valid = self._validate_columns(train_df, schema)
82
+ test_valid = self._validate_columns(test_df, schema)
83
+
84
+ if not (train_valid and test_valid):
85
+ message = "Column validation failed"
86
+ logging.error(message)
87
+ validation_status = False
88
+ else:
89
+ message = "Validation passed"
90
+ validation_status = True
91
+
92
+ # Drift detection (warning only, does not fail validation)
93
+ drift_report = self._detect_drift(train_df, test_df)
94
+ drifted_cols = [c for c, r in drift_report.items() if r["drift_detected"]]
95
+ if drifted_cols:
96
+ message += f" | Drift detected in: {drifted_cols}"
97
+ logging.warning(f"Drift found in {len(drifted_cols)} columns")
98
+
99
+ # Save drift report
100
+ os.makedirs(os.path.dirname(self.config.drift_report_file_path), exist_ok=True)
101
+ with open(self.config.drift_report_file_path, "w") as f:
102
+ yaml.dump(drift_report, f, default_flow_style=False)
103
+ logging.info(f"Drift report saved to {self.config.drift_report_file_path}")
104
+
105
+ return DataValidationArtifact(
106
+ validation_status=validation_status,
107
+ message=message,
108
+ drift_report_file_path=self.config.drift_report_file_path,
109
+ )
110
+
111
+ except Exception as e:
112
+ raise visaException(e, sys) from e
visa_approval_prediction/components/model_evaluation.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import pickle
4
+ import shutil
5
+
6
+ import pandas as pd
7
+ from sklearn.metrics import f1_score
8
+
9
+ from visa_approval_prediction.constants import TARGET_COLUMN
10
+ from visa_approval_prediction.entity.config_entity import ModelEvaluationConfig
11
+ from visa_approval_prediction.entity.artifact_entity import (
12
+ DataIngestionArtifact,
13
+ ModelTrainerArtifact,
14
+ ModelEvaluationArtifact,
15
+ )
16
+ from visa_approval_prediction.exception import visaException
17
+ from visa_approval_prediction.logger import logging
18
+
19
+
20
+ class ModelEvaluation:
21
+ def __init__(
22
+ self,
23
+ config: ModelEvaluationConfig,
24
+ trainer_artifact: ModelTrainerArtifact,
25
+ ingestion_artifact: DataIngestionArtifact,
26
+ ):
27
+ self.config = config
28
+ self.trainer_artifact = trainer_artifact
29
+ self.ingestion_artifact = ingestion_artifact
30
+
31
+ def initiate_model_evaluation(self) -> ModelEvaluationArtifact:
32
+ logging.info("Starting model evaluation")
33
+ try:
34
+ new_model_f1 = self.trainer_artifact.test_f1_score
35
+ best_model_f1 = 0.0
36
+
37
+ # Compare with existing production model if it exists
38
+ if os.path.exists(self.config.best_model_path):
39
+ logging.info(
40
+ f"Existing model found at {self.config.best_model_path}"
41
+ )
42
+ with open(self.config.best_model_path, "rb") as f:
43
+ existing_model = pickle.load(f)
44
+
45
+ # Use raw test data — each visaModel has its own preprocessor
46
+ # so we call predict() on raw DataFrame, not pre-transformed arrays
47
+ test_df = pd.read_csv(self.ingestion_artifact.test_file_path)
48
+ X_test = test_df.drop(columns=[TARGET_COLUMN])
49
+ y_test = test_df[TARGET_COLUMN]
50
+
51
+ y_pred = existing_model.predict(X_test)
52
+ best_model_f1 = f1_score(y_test, y_pred)
53
+ logging.info(f"Existing model F1: {best_model_f1:.4f}")
54
+ logging.info(f"New model F1: {new_model_f1:.4f}")
55
+ else:
56
+ logging.info("No existing model found, new model will be accepted")
57
+
58
+ improved = new_model_f1 - best_model_f1
59
+ is_accepted = (
60
+ improved >= self.config.changed_threshold_score
61
+ or best_model_f1 == 0.0
62
+ )
63
+
64
+ if is_accepted:
65
+ os.makedirs(
66
+ os.path.dirname(self.config.best_model_path), exist_ok=True
67
+ )
68
+ shutil.copy2(
69
+ self.trainer_artifact.trained_model_file_path,
70
+ self.config.best_model_path,
71
+ )
72
+ logging.info(
73
+ f"New model promoted to {self.config.best_model_path}"
74
+ )
75
+ else:
76
+ logging.info(
77
+ f"New model rejected (improvement {improved:.4f} "
78
+ f"< threshold {self.config.changed_threshold_score})"
79
+ )
80
+
81
+ return ModelEvaluationArtifact(
82
+ is_model_accepted=is_accepted,
83
+ best_model_path=self.config.best_model_path,
84
+ trained_model_f1_score=new_model_f1,
85
+ best_model_f1_score=(
86
+ max(best_model_f1, new_model_f1) if is_accepted else best_model_f1
87
+ ),
88
+ )
89
+
90
+ except Exception as e:
91
+ raise visaException(e, sys) from e
visa_approval_prediction/components/model_trainer.py ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import pickle
4
+
5
+ import yaml
6
+ import numpy as np
7
+ from sklearn.model_selection import GridSearchCV
8
+ from sklearn.metrics import f1_score, accuracy_score, classification_report
9
+ from sklearn.ensemble import GradientBoostingClassifier, RandomForestClassifier
10
+ from xgboost import XGBClassifier
11
+
12
+ from visa_approval_prediction.entity.config_entity import ModelTrainerConfig
13
+ from visa_approval_prediction.entity.artifact_entity import (
14
+ DataTransformationArtifact,
15
+ ModelTrainerArtifact,
16
+ )
17
+ from visa_approval_prediction.entity.estimator import visaModel
18
+ from visa_approval_prediction.exception import visaException
19
+ from visa_approval_prediction.logger import logging
20
+
21
+ MODEL_CLASSES = {
22
+ "RandomForestClassifier": RandomForestClassifier,
23
+ "GradientBoostingClassifier": GradientBoostingClassifier,
24
+ "XGBClassifier": XGBClassifier,
25
+ }
26
+
27
+
28
+ class ModelTrainer:
29
+ def __init__(
30
+ self,
31
+ config: ModelTrainerConfig,
32
+ transformation_artifact: DataTransformationArtifact,
33
+ ):
34
+ self.config = config
35
+ self.transformation_artifact = transformation_artifact
36
+
37
+ def initiate_model_training(self) -> ModelTrainerArtifact:
38
+ logging.info("Starting model training")
39
+ try:
40
+ # Load transformed + resampled training data
41
+ X_train = np.load(
42
+ self.transformation_artifact.transformed_train_file_path
43
+ )
44
+ y_train = np.load(
45
+ self.transformation_artifact.transformed_train_target_path
46
+ )
47
+ X_test = np.load(
48
+ self.transformation_artifact.transformed_test_file_path
49
+ )
50
+ y_test = np.load(
51
+ self.transformation_artifact.transformed_test_target_path
52
+ )
53
+ logging.info(f"Train: {X_train.shape}, Test: {X_test.shape}")
54
+
55
+ # Load preprocessor (needed to bundle into visaModel)
56
+ with open(
57
+ self.transformation_artifact.preprocessor_object_file_path, "rb"
58
+ ) as f:
59
+ preprocessor = pickle.load(f)
60
+
61
+ # Load model config
62
+ with open(self.config.model_config_file_path, "r") as f:
63
+ config = yaml.safe_load(f)
64
+
65
+ gs_cfg = config["grid_search"]["params"]
66
+ models_cfg = config["model_selection"]
67
+
68
+ # Plain GridSearchCV for each model on pre-resampled data
69
+ results = []
70
+ for key in sorted(models_cfg.keys()):
71
+ mcfg = models_cfg[key]
72
+ cls_name = mcfg["class"]
73
+ cls = MODEL_CLASSES[cls_name]
74
+ base_params = dict(mcfg.get("params", {}))
75
+ param_grid = mcfg.get("search_param_grid", {})
76
+
77
+ logging.info(f"Training {cls_name} ...")
78
+ estimator = cls(**base_params)
79
+
80
+ gs = GridSearchCV(
81
+ estimator,
82
+ param_grid=param_grid,
83
+ cv=gs_cfg["cv"],
84
+ scoring=gs_cfg["scoring"],
85
+ verbose=gs_cfg.get("verbose", 0),
86
+ n_jobs=-1,
87
+ )
88
+ gs.fit(X_train, y_train)
89
+
90
+ # Evaluate on unresampled test data
91
+ y_pred_train = gs.best_estimator_.predict(X_train)
92
+ y_pred_test = gs.best_estimator_.predict(X_test)
93
+ train_f1 = f1_score(y_train, y_pred_train)
94
+ test_f1 = f1_score(y_test, y_pred_test)
95
+ test_acc = accuracy_score(y_test, y_pred_test)
96
+
97
+ results.append(
98
+ {
99
+ "name": cls_name,
100
+ "best_params": gs.best_params_,
101
+ "cv_f1": gs.best_score_,
102
+ "train_f1": train_f1,
103
+ "test_f1": test_f1,
104
+ "test_acc": test_acc,
105
+ "model": gs.best_estimator_,
106
+ }
107
+ )
108
+ logging.info(
109
+ f" {cls_name}: CV F1={gs.best_score_:.4f}, "
110
+ f"Test F1={test_f1:.4f}, Test Acc={test_acc:.4f}"
111
+ )
112
+
113
+ # Select best model by test F1
114
+ best = max(results, key=lambda r: r["test_f1"])
115
+ logging.info(
116
+ f"Best model: {best['name']} (Test F1={best['test_f1']:.4f})"
117
+ )
118
+
119
+ # Log classification report
120
+ y_pred_best = best["model"].predict(X_test)
121
+ report = classification_report(
122
+ y_test, y_pred_best, target_names=["Certified", "Denied"]
123
+ )
124
+ logging.info(f"Classification report:\n{report}")
125
+
126
+ if best["test_f1"] < self.config.expected_accuracy:
127
+ logging.warning(
128
+ f"Best F1 ({best['test_f1']:.4f}) below threshold "
129
+ f"({self.config.expected_accuracy})"
130
+ )
131
+
132
+ # Bundle preprocessor + best classifier into visaModel
133
+ visa_model = visaModel(
134
+ preprocessing_object=preprocessor,
135
+ trained_model_object=best["model"],
136
+ )
137
+
138
+ os.makedirs(
139
+ os.path.dirname(self.config.trained_model_file_path), exist_ok=True
140
+ )
141
+ with open(self.config.trained_model_file_path, "wb") as f:
142
+ pickle.dump(visa_model, f)
143
+ logging.info(f"Model saved to {self.config.trained_model_file_path}")
144
+
145
+ return ModelTrainerArtifact(
146
+ trained_model_file_path=self.config.trained_model_file_path,
147
+ train_f1_score=best["train_f1"],
148
+ test_f1_score=best["test_f1"],
149
+ test_accuracy=best["test_acc"],
150
+ best_model_name=best["name"],
151
+ )
152
+
153
+ except Exception as e:
154
+ raise visaException(e, sys) from e
visa_approval_prediction/entity/artifact_entity.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class DataIngestionArtifact:
6
+ train_file_path: str
7
+ test_file_path: str
8
+
9
+
10
+ @dataclass
11
+ class DataValidationArtifact:
12
+ validation_status: bool
13
+ message: str
14
+ drift_report_file_path: str
15
+
16
+
17
+ @dataclass
18
+ class DataTransformationArtifact:
19
+ transformed_train_file_path: str
20
+ transformed_test_file_path: str
21
+ transformed_train_target_path: str
22
+ transformed_test_target_path: str
23
+ preprocessor_object_file_path: str
24
+
25
+
26
+ @dataclass
27
+ class ModelTrainerArtifact:
28
+ trained_model_file_path: str
29
+ train_f1_score: float
30
+ test_f1_score: float
31
+ test_accuracy: float
32
+ best_model_name: str
33
+
34
+
35
+ @dataclass
36
+ class ModelEvaluationArtifact:
37
+ is_model_accepted: bool
38
+ best_model_path: str
39
+ trained_model_f1_score: float
40
+ best_model_f1_score: float
visa_approval_prediction/entity/config_entity.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from datetime import datetime
3
+ from visa_approval_prediction.constants import (
4
+ ARTIFACT_DIR,
5
+ DATA_INGESTION_DIR_NAME,
6
+ DATA_INGESTION_TRAIN_TEST_SPLIT_RATIO,
7
+ TRAIN_FILE_NAME,
8
+ TEST_FILE_NAME,
9
+ DATA_VALIDATION_DIR_NAME,
10
+ DATA_VALIDATION_DRIFT_REPORT_DIR,
11
+ DATA_VALIDATION_DRIFT_REPORT_FILE_NAME,
12
+ SCHEMA_FILE_PATH,
13
+ DATA_TRANSFORMATION_DIR_NAME,
14
+ DATA_TRANSFORMATION_TRANSFORMED_DATA_DIR,
15
+ DATA_TRANSFORMATION_TRANSFORMED_OBJECT_DIR,
16
+ PREPROCSSING_OBJECT_FILE_NAME,
17
+ MODEL_TRAINER_DIR_NAME,
18
+ MODEL_TRAINER_TRAINED_MODEL_DIR,
19
+ MODEL_TRAINER_TRAINED_MODEL_NAME,
20
+ MODEL_TRAINER_EXPECTED_SCORE,
21
+ MODEL_TRAINER_MODEL_CONFIG_FILE_PATH,
22
+ MODEL_EVALUATION_CHANGED_THRESHOLD_SCORE,
23
+ MODEL_FILE_NAME,
24
+ )
25
+
26
+
27
+ class TrainingPipelineConfig:
28
+ def __init__(self, timestamp=None):
29
+ self.timestamp = timestamp or datetime.now().strftime("%m_%d_%Y_%H_%M_%S")
30
+ self.artifact_dir = os.path.join(ARTIFACT_DIR, self.timestamp)
31
+
32
+
33
+ class DataIngestionConfig:
34
+ def __init__(self, training_pipeline_config: TrainingPipelineConfig):
35
+ self.data_source_path = "EasyVisa.csv"
36
+ self.split_ratio = DATA_INGESTION_TRAIN_TEST_SPLIT_RATIO
37
+ ingestion_dir = os.path.join(
38
+ training_pipeline_config.artifact_dir, DATA_INGESTION_DIR_NAME
39
+ )
40
+ self.train_file_path = os.path.join(ingestion_dir, TRAIN_FILE_NAME)
41
+ self.test_file_path = os.path.join(ingestion_dir, TEST_FILE_NAME)
42
+
43
+
44
+ class DataValidationConfig:
45
+ def __init__(self, training_pipeline_config: TrainingPipelineConfig):
46
+ validation_dir = os.path.join(
47
+ training_pipeline_config.artifact_dir, DATA_VALIDATION_DIR_NAME
48
+ )
49
+ self.schema_file_path = SCHEMA_FILE_PATH
50
+ self.drift_report_file_path = os.path.join(
51
+ validation_dir,
52
+ DATA_VALIDATION_DRIFT_REPORT_DIR,
53
+ DATA_VALIDATION_DRIFT_REPORT_FILE_NAME,
54
+ )
55
+
56
+
57
+ class DataTransformationConfig:
58
+ def __init__(self, training_pipeline_config: TrainingPipelineConfig):
59
+ transformation_dir = os.path.join(
60
+ training_pipeline_config.artifact_dir, DATA_TRANSFORMATION_DIR_NAME
61
+ )
62
+ data_dir = os.path.join(
63
+ transformation_dir, DATA_TRANSFORMATION_TRANSFORMED_DATA_DIR
64
+ )
65
+ object_dir = os.path.join(
66
+ transformation_dir, DATA_TRANSFORMATION_TRANSFORMED_OBJECT_DIR
67
+ )
68
+ self.transformed_train_file_path = os.path.join(data_dir, "train.npy")
69
+ self.transformed_test_file_path = os.path.join(data_dir, "test.npy")
70
+ self.transformed_train_target_path = os.path.join(data_dir, "train_target.npy")
71
+ self.transformed_test_target_path = os.path.join(data_dir, "test_target.npy")
72
+ self.preprocessor_object_file_path = os.path.join(
73
+ object_dir, PREPROCSSING_OBJECT_FILE_NAME
74
+ )
75
+
76
+
77
+ class ModelTrainerConfig:
78
+ def __init__(self, training_pipeline_config: TrainingPipelineConfig):
79
+ trainer_dir = os.path.join(
80
+ training_pipeline_config.artifact_dir, MODEL_TRAINER_DIR_NAME
81
+ )
82
+ self.trained_model_file_path = os.path.join(
83
+ trainer_dir, MODEL_TRAINER_TRAINED_MODEL_DIR, MODEL_TRAINER_TRAINED_MODEL_NAME
84
+ )
85
+ self.expected_accuracy = MODEL_TRAINER_EXPECTED_SCORE
86
+ self.model_config_file_path = MODEL_TRAINER_MODEL_CONFIG_FILE_PATH
87
+
88
+
89
+ class ModelEvaluationConfig:
90
+ def __init__(self, training_pipeline_config: TrainingPipelineConfig):
91
+ self.changed_threshold_score = MODEL_EVALUATION_CHANGED_THRESHOLD_SCORE
92
+ self.best_model_path = os.path.join(ARTIFACT_DIR, MODEL_FILE_NAME)
visa_approval_prediction/entity/estimator.py CHANGED
@@ -16,6 +16,31 @@ class TargetValueMapping:
16
 
17
 
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  class visaModel:
20
  def __init__(self, preprocessing_object: Pipeline, trained_model_object: object):
21
  """
 
16
 
17
 
18
 
19
+ class ThresholdClassifier:
20
+ """Wraps a trained model and applies a custom probability threshold for predict()."""
21
+ def __init__(self, base_model, threshold=0.5):
22
+ self.base_model = base_model
23
+ self.threshold = threshold
24
+
25
+ def predict(self, X):
26
+ import numpy as np
27
+ proba = self.base_model.predict_proba(X)[:, 1]
28
+ return (proba >= self.threshold).astype(int)
29
+
30
+ def predict_proba(self, X):
31
+ return self.base_model.predict_proba(X)
32
+
33
+ @property
34
+ def classes_(self):
35
+ return self.base_model.classes_
36
+
37
+ def __repr__(self):
38
+ return f"ThresholdClassifier({type(self.base_model).__name__}, threshold={self.threshold:.3f})"
39
+
40
+ def __str__(self):
41
+ return self.__repr__()
42
+
43
+
44
  class visaModel:
45
  def __init__(self, preprocessing_object: Pipeline, trained_model_object: object):
46
  """
visa_approval_prediction/pipeline/training_pipeline.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+
3
+ from visa_approval_prediction.entity.config_entity import (
4
+ TrainingPipelineConfig,
5
+ DataIngestionConfig,
6
+ DataValidationConfig,
7
+ DataTransformationConfig,
8
+ ModelTrainerConfig,
9
+ ModelEvaluationConfig,
10
+ )
11
+ from visa_approval_prediction.components.data_ingestion import DataIngestion
12
+ from visa_approval_prediction.components.data_validation import DataValidation
13
+ from visa_approval_prediction.components.data_transformation import DataTransformation
14
+ from visa_approval_prediction.components.model_trainer import ModelTrainer
15
+ from visa_approval_prediction.components.model_evaluation import ModelEvaluation
16
+ from visa_approval_prediction.exception import visaException
17
+ from visa_approval_prediction.logger import logging
18
+
19
+
20
+ class TrainingPipeline:
21
+ def __init__(self):
22
+ self.pipeline_config = TrainingPipelineConfig()
23
+
24
+ def start_data_ingestion(self):
25
+ config = DataIngestionConfig(self.pipeline_config)
26
+ component = DataIngestion(config)
27
+ return component.initiate_data_ingestion()
28
+
29
+ def start_data_validation(self, ingestion_artifact):
30
+ config = DataValidationConfig(self.pipeline_config)
31
+ component = DataValidation(config, ingestion_artifact)
32
+ return component.initiate_data_validation()
33
+
34
+ def start_data_transformation(self, ingestion_artifact):
35
+ config = DataTransformationConfig(self.pipeline_config)
36
+ component = DataTransformation(config, ingestion_artifact)
37
+ return component.initiate_data_transformation()
38
+
39
+ def start_model_training(self, transformation_artifact):
40
+ config = ModelTrainerConfig(self.pipeline_config)
41
+ component = ModelTrainer(config, transformation_artifact)
42
+ return component.initiate_model_training()
43
+
44
+ def start_model_evaluation(self, trainer_artifact, ingestion_artifact):
45
+ config = ModelEvaluationConfig(self.pipeline_config)
46
+ component = ModelEvaluation(config, trainer_artifact, ingestion_artifact)
47
+ return component.initiate_model_evaluation()
48
+
49
+ def run(self):
50
+ try:
51
+ print("=" * 60)
52
+ print("VISA APPROVAL PREDICTION - TRAINING PIPELINE")
53
+ print("=" * 60)
54
+
55
+ # Stage 1: Data Ingestion
56
+ print("\n[1/5] Data Ingestion ...")
57
+ logging.info(">>> Stage 1: Data Ingestion")
58
+ ingestion_artifact = self.start_data_ingestion()
59
+ print(f" Train: {ingestion_artifact.train_file_path}")
60
+ print(f" Test: {ingestion_artifact.test_file_path}")
61
+
62
+ # Stage 2: Data Validation
63
+ print("\n[2/5] Data Validation ...")
64
+ logging.info(">>> Stage 2: Data Validation")
65
+ validation_artifact = self.start_data_validation(ingestion_artifact)
66
+ print(f" Status: {validation_artifact.message}")
67
+ if not validation_artifact.validation_status:
68
+ raise Exception(
69
+ f"Data validation failed: {validation_artifact.message}"
70
+ )
71
+
72
+ # Stage 3: Data Transformation
73
+ print("\n[3/5] Data Transformation ...")
74
+ logging.info(">>> Stage 3: Data Transformation")
75
+ transformation_artifact = self.start_data_transformation(
76
+ ingestion_artifact
77
+ )
78
+ print(f" Preprocessor: {transformation_artifact.preprocessor_object_file_path}")
79
+
80
+ # Stage 4: Model Training
81
+ print("\n[4/5] Model Training (this may take a while) ...")
82
+ logging.info(">>> Stage 4: Model Training")
83
+ trainer_artifact = self.start_model_training(transformation_artifact)
84
+ print(f" Best model: {trainer_artifact.best_model_name}")
85
+ print(f" Test Acc: {trainer_artifact.test_accuracy:.4f}")
86
+ print(f" Test F1: {trainer_artifact.test_f1_score:.4f}")
87
+
88
+ # Stage 5: Model Evaluation
89
+ print("\n[5/5] Model Evaluation ...")
90
+ logging.info(">>> Stage 5: Model Evaluation")
91
+ evaluation_artifact = self.start_model_evaluation(
92
+ trainer_artifact, ingestion_artifact
93
+ )
94
+ print(f" New model F1: {evaluation_artifact.trained_model_f1_score:.4f}")
95
+ print(f" Existing model F1: {evaluation_artifact.best_model_f1_score:.4f}")
96
+ print(f" Accepted: {evaluation_artifact.is_model_accepted}")
97
+ print(f" Model: {evaluation_artifact.best_model_path}")
98
+
99
+ print("\n" + "=" * 60)
100
+ print("PIPELINE COMPLETE")
101
+ print("=" * 60)
102
+
103
+ logging.info("Training pipeline finished successfully")
104
+ return evaluation_artifact
105
+
106
+ except Exception as e:
107
+ logging.error(f"Training pipeline failed: {e}")
108
+ raise visaException(e, sys) from e
109
+
110
+
111
+ if __name__ == "__main__":
112
+ pipeline = TrainingPipeline()
113
+ pipeline.run()