├── industrial_emissions_synthetic_data.xlsx ├── README.md ├── Industrial-Emissions-Prediction-Using-Regression-Models.py └── file /industrial_emissions_synthetic_data.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nelvinebi/Industrial-Emissions-Prediction-Using-Regression-Models/HEAD/industrial_emissions_synthetic_data.xlsx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 📌 About 2 | This project uses regression models to predict industrial emissions based on synthetic operational data. It demonstrates how machine learning can support environmental monitoring and policy decision-making. 3 | 4 | 📊 Dataset 5 | Size: >100 synthetic data points 6 | 7 | Features: Production Output, Energy Consumption, Waste Generated, Operational Hours 8 | 9 | Target: Industrial Emissions (in tons/year) 10 | 11 | 🛠️ Technologies Used 12 | Python 13 | 14 | Pandas, NumPy 15 | 16 | Scikit-learn 17 | 18 | Matplotlib, Seaborn 19 | 20 | 🚀 How to Run 21 | Clone this repository: 22 | 23 | bash 24 | Copy 25 | Edit 26 | git clone https://github.com/Nelvinebi/Industrial-Emissions-Prediction-Using-Regression-Models.git 27 | cd Industrial-Emissions-Prediction-Using-Regression-Models 28 | Install dependencies: 29 | 30 | bash 31 | Copy 32 | Edit 33 | pip install -r requirements.txt 34 | Run the main script: 35 | 36 | bash 37 | Copy 38 | Edit 39 | python industrial_emissions_prediction.py 40 | 📂 Files in Repository 41 | industrial_emissions_prediction.py → Main analysis and prediction script 42 | 43 | industrial_emissions_synthetic_data.xlsx → Synthetic dataset 44 | 45 | README.md → Project documentation 46 | 47 | 👤 Author 48 | Name: Agbozu Ebingiye Nelvin 49 | 📧 Email: nelvinebingiye@gmail.com 50 | 🌐 GitHub: github.com/Nelvinebi 51 | -------------------------------------------------------------------------------- /Industrial-Emissions-Prediction-Using-Regression-Models.py: -------------------------------------------------------------------------------- 1 | # Industrial-Emissions-Prediction-Using-Regression-Models 2 | # ------------------------------------------------------- 3 | # Generates a synthetic industrial dataset and trains regression models to predict 4 | # annual CO2-equivalent emissions (tons). Saves dataset, model performance metrics, 5 | # plots, and trained model files. 6 | # 7 | # Outputs: 8 | # - ./data/industrial_emissions_synthetic.csv 9 | # - ./results/performance_table.csv 10 | # - ./results/observed_vs_pred_.png 11 | # - ./results/feature_importances_.png (for tree models) 12 | # - ./models/.joblib 13 | # 14 | # Dependencies: 15 | # pip install numpy pandas matplotlib seaborn scikit-learn joblib openpyxl 16 | 17 | import os 18 | import numpy as np 19 | import pandas as pd 20 | import matplotlib.pyplot as plt 21 | import seaborn as sns 22 | from sklearn.model_selection import train_test_split 23 | from sklearn.pipeline import Pipeline 24 | from sklearn.preprocessing import StandardScaler, OneHotEncoder 25 | from sklearn.compose import ColumnTransformer 26 | from sklearn.linear_model import LinearRegression, Ridge 27 | from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor 28 | from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score 29 | import joblib 30 | 31 | RND = 42 32 | 33 | def generate_synthetic_industrial_data(n=500, random_state=RND): 34 | rng = np.random.default_rng(random_state) 35 | plant_ids = [f"P{str(i).zfill(4)}" for i in range(n)] 36 | production_volume = np.round(rng.normal(200000, 80000, size=n)).clip(20000, None) 37 | fuel_types = rng.choice(["coal", "gas", "oil", "biomass"], size=n, p=[0.35, 0.4, 0.2, 0.05]) 38 | operating_hours = np.round(rng.normal(7000, 800, size=n)).clip(1000, 8760) 39 | efficiency = np.round(np.clip(rng.normal(0.85, 0.08, size=n), 0.4, 0.98), 3) 40 | maintenance_score = np.round(np.clip(rng.normal(70, 15, size=n), 10, 100), 1) 41 | ambient_temperature_C = np.round(rng.normal(25, 8, size=n), 2) 42 | ambient_humidity = np.round(np.clip(rng.normal(0.6, 0.15, size=n), 0.1, 0.99), 3) 43 | 44 | fuel_emission_factor = np.array([2.5 if f=="coal" else 1.2 if f=="gas" else 1.8 if f=="oil" else 0.9 for f in fuel_types]) 45 | base_emissions = production_volume * fuel_emission_factor * 1e-3 46 | eff_factor = (1.0 / efficiency) 47 | maint_factor = (1.0 + (50 - maintenance_score) / 200.0) 48 | hours_factor = 1.0 + (operating_hours - 7000) / 20000.0 49 | met_factor = 1.0 + 0.002 * (ambient_temperature_C - 20) + 0.05 * (ambient_humidity - 0.5) 50 | 51 | emissions = base_emissions * eff_factor * maint_factor * hours_factor * met_factor 52 | coal_penalty = np.where(fuel_types=="coal", 1.0 + (50 - maintenance_score)/150.0, 1.0) 53 | emissions *= coal_penalty 54 | noise = rng.normal(scale=0.05 * emissions) 55 | emissions = np.maximum(0.0, emissions + noise) 56 | 57 | df = pd.DataFrame({ 58 | "plant_id": plant_ids, 59 | "production_volume_tons_per_year": production_volume, 60 | "fuel_type": fuel_types, 61 | "operating_hours_per_year": operating_hours, 62 | "efficiency": efficiency, 63 | "maintenance_score": maintenance_score, 64 | "ambient_temperature_C": ambient_temperature_C, 65 | "ambient_humidity": ambient_humidity, 66 | "emissions_tons_CO2eq": np.round(emissions, 3) 67 | }) 68 | 69 | return df 70 | 71 | def build_preprocessor(): 72 | numeric_features = ["production_volume_tons_per_year", "operating_hours_per_year", 73 | "efficiency", "maintenance_score", "ambient_temperature_C", "ambient_humidity"] 74 | numeric_transformer = Pipeline([("scaler", StandardScaler())]) 75 | categorical_features = ["fuel_type"] 76 | categorical_transformer = Pipeline([("onehot", OneHotEncoder(drop="first"))]) 77 | 78 | preprocessor = ColumnTransformer(transformers=[ 79 | ("num", numeric_transformer, numeric_features), 80 | ("cat", categorical_transformer, categorical_features) 81 | ]) 82 | return preprocessor, numeric_features, categorical_features 83 | 84 | def train_models(df, out_dir=".", random_state=RND): 85 | os.makedirs(out_dir, exist_ok=True) 86 | os.makedirs(os.path.join(out_dir, "results"), exist_ok=True) 87 | os.makedirs(os.path.join(out_dir, "models"), exist_ok=True) 88 | os.makedirs(os.path.join(out_dir, "data"), exist_ok=True) 89 | 90 | X = df.drop(columns=["plant_id", "emissions_tons_CO2eq"]) 91 | y = df["emissions_tons_CO2eq"].values 92 | 93 | preprocessor, numeric_features, categorical_features = build_preprocessor() 94 | X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=random_state) 95 | 96 | models = { 97 | "LinearRegression": Pipeline([("pre", preprocessor), ("lr", LinearRegression())]), 98 | "Ridge": Pipeline([("pre", preprocessor), ("ridge", Ridge(alpha=1.0, random_state=random_state))]), 99 | "RandomForest": Pipeline([("pre", preprocessor), ("rf", RandomForestRegressor(n_estimators=200, random_state=random_state))]), 100 | "GradientBoosting": Pipeline([("pre", preprocessor), ("gb", GradientBoostingRegressor(n_estimators=200, learning_rate=0.05, random_state=random_state))]) 101 | } 102 | 103 | perf_records = [] 104 | trained_models = {} 105 | 106 | for name, model in models.items(): 107 | print(f"Training {name} ...") 108 | model.fit(X_train, y_train) 109 | y_pred = model.predict(X_test) 110 | rmse = mean_squared_error(y_test, y_pred, squared=False) 111 | mae = mean_absolute_error(y_test, y_pred) 112 | r2 = r2_score(y_test, y_pred) 113 | perf_records.append({"model": name, "RMSE": rmse, "MAE": mae, "R2": r2}) 114 | trained_models[name] = model 115 | 116 | plt.figure(figsize=(6,5)) 117 | plt.scatter(y_test, y_pred, alpha=0.7) 118 | mmin = min(y_test.min(), y_pred.min()) 119 | mmax = max(y_test.max(), y_pred.max()) 120 | plt.plot([mmin, mmax], [mmin, mmax], linestyle="--", color="red") 121 | plt.xlabel("Observed emissions (tons CO2-eq)") 122 | plt.ylabel("Predicted emissions (tons CO2-eq)") 123 | plt.title(f"Observed vs Predicted - {name}") 124 | plt.tight_layout() 125 | plot_path = os.path.join(out_dir, "results", f"observed_vs_pred_{name}.png") 126 | plt.savefig(plot_path, dpi=150) 127 | plt.close() 128 | 129 | if name in ["RandomForest", "GradientBoosting"]: 130 | pre = model.named_steps["pre"] 131 | num_feats = numeric_features 132 | ohe = pre.named_transformers_["cat"].named_steps["onehot"] 133 | cat_cols = ohe.get_feature_names_out(categorical_features).tolist() 134 | feat_names = num_feats + cat_cols 135 | importances = model.named_steps[name.lower() if name!="GradientBoosting" else "gb"].feature_importances_ 136 | fi_series = pd.Series(importances, index=feat_names).sort_values(ascending=False) 137 | fi_path = os.path.join(out_dir, "results", f"feature_importances_{name}.png") 138 | plt.figure(figsize=(8,4)) 139 | sns.barplot(x=fi_series.values, y=fi_series.index) 140 | plt.title(f"Feature importances - {name}") 141 | plt.xlabel("Importance") 142 | plt.tight_layout() 143 | plt.savefig(fi_path, dpi=150) 144 | plt.close() 145 | fi_series.to_csv(os.path.join(out_dir, "results", f"feature_importances_{name}.csv")) 146 | 147 | joblib.dump(model, os.path.join(out_dir, "models", f"{name}.joblib")) 148 | 149 | perf_df = pd.DataFrame(perf_records).set_index("model") 150 | perf_df.to_csv(os.path.join(out_dir, "results", "performance_table.csv")) 151 | print("\nModel performance:\n", perf_df.round(4)) 152 | 153 | best_model_name = perf_df["RMSE"].idxmin() 154 | best_model = trained_models[best_model_name] 155 | y_pred_best = best_model.predict(X_test) 156 | preds_df = X_test.copy().reset_index(drop=True) 157 | preds_df["observed_emissions"] = y_test 158 | preds_df["predicted_emissions"] = np.round(y_pred_best, 3) 159 | preds_df.to_csv(os.path.join(out_dir, "results", "test_predictions_best_model.csv"), index=False) 160 | 161 | return perf_df, trained_models 162 | 163 | def main(): 164 | out_dir = "industrial_emissions_project_output" 165 | os.makedirs(out_dir, exist_ok=True) 166 | 167 | print("Generating synthetic dataset...") 168 | df = generate_synthetic_industrial_data(n=500, random_state=RND) 169 | data_path = os.path.join(out_dir, "data", "industrial_emissions_synthetic.csv") 170 | os.makedirs(os.path.dirname(data_path), exist_ok=True) 171 | df.to_csv(data_path, index=False) 172 | print("Saved synthetic dataset to:", data_path) 173 | 174 | os.makedirs(os.path.join(out_dir, "results"), exist_ok=True) 175 | plt.figure(figsize=(6,4)) 176 | sns.histplot(df["emissions_tons_CO2eq"], bins=40, kde=True) 177 | plt.xlabel("Emissions (tons CO2-eq)") 178 | plt.title("Distribution of Synthetic Emissions") 179 | plt.tight_layout() 180 | plt.savefig(os.path.join(out_dir, "results", "emissions_distribution.png"), dpi=150) 181 | plt.close() 182 | 183 | perf_df, models = train_models(df, out_dir=out_dir) 184 | 185 | print("\nAll outputs saved under:", out_dir) 186 | print("Models saved to:", os.path.join(out_dir, "models")) 187 | print("Results saved to:", os.path.join(out_dir, "results")) 188 | 189 | if __name__ == "__main__": 190 | main() 191 | -------------------------------------------------------------------------------- /file: -------------------------------------------------------------------------------- 1 | """ 2 | Industrial-Emissions-Prediction-Using-Regression-Models 3 | 4 | Generates a synthetic industrial dataset and trains regression models to predict 5 | annual CO2-equivalent emissions (tons). Saves dataset, model performance metrics, 6 | plots, and trained model files. 7 | 8 | Outputs: 9 | - ./data/industrial_emissions_synthetic.csv 10 | - ./results/performance_table.csv 11 | - ./results/observed_vs_pred_.png 12 | - ./results/feature_importances_.png (for tree models) 13 | - ./models/.joblib 14 | 15 | Dependencies: 16 | pip install numpy pandas matplotlib seaborn scikit-learn joblib openpyxl 17 | """ 18 | 19 | import os 20 | import numpy as np 21 | import pandas as pd 22 | import matplotlib.pyplot as plt 23 | import seaborn as sns 24 | from sklearn.model_selection import train_test_split 25 | from sklearn.pipeline import Pipeline 26 | from sklearn.preprocessing import StandardScaler, OneHotEncoder 27 | from sklearn.compose import ColumnTransformer 28 | from sklearn.linear_model import LinearRegression, Ridge 29 | from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor 30 | from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score 31 | import joblib 32 | 33 | RND = 42 34 | 35 | def generate_synthetic_industrial_data(n=500, random_state=RND): 36 | """ 37 | Generate synthetic industrial emissions dataset with n rows. 38 | Columns: 39 | - plant_id (categorical) 40 | - production_volume (tons/year) 41 | - fuel_type (categorical: coal, gas, oil, biomass) 42 | - operating_hours (hours/year) 43 | - efficiency (0-1) 44 | - maintenance_score (0-100) 45 | - ambient_temperature_C 46 | - ambient_humidity (0-1) 47 | - emissions_tons (target, CO2-eq tons/year) 48 | """ 49 | rng = np.random.default_rng(random_state) 50 | plant_ids = [f"P{str(i).zfill(4)}" for i in range(n)] 51 | production_volume = np.round(rng.normal(200000, 80000, size=n)).clip(20000, None) # tons/year 52 | fuel_types = rng.choice(["coal", "gas", "oil", "biomass"], size=n, p=[0.35, 0.4, 0.2, 0.05]) 53 | operating_hours = np.round(rng.normal(7000, 800, size=n)).clip(1000, 8760) # hours/year 54 | efficiency = np.round(np.clip(rng.normal(0.85, 0.08, size=n), 0.4, 0.98), 3) # process efficiency 55 | maintenance_score = np.round(np.clip(rng.normal(70, 15, size=n), 10, 100), 1) # 0-100 56 | ambient_temperature_C = np.round(rng.normal(25, 8, size=n), 2) 57 | ambient_humidity = np.round(np.clip(rng.normal(0.6, 0.15, size=n), 0.1, 0.99), 3) 58 | 59 | # Fuel emission factors (CO2-eq per unit production) - synthetic proxies 60 | fuel_emission_factor = np.array([2.5 if f=="coal" else 1.2 if f=="gas" else 1.8 if f=="oil" else 0.9 for f in fuel_types]) 61 | # Base emissions scale with production and fuel factor, reduced by efficiency and maintenance 62 | base_emissions = production_volume * fuel_emission_factor * 1e-3 # scale to tons CO2-eq 63 | # Efficiency and maintenance reduce emissions 64 | eff_factor = (1.0 / efficiency) # lower efficiency increases emissions 65 | maint_factor = (1.0 + (50 - maintenance_score) / 200.0) # worse maintenance slightly increases emissions 66 | # Operating hours effect (more hours -> more throughput and also potential inefficiencies) 67 | hours_factor = 1.0 + (operating_hours - 7000) / 20000.0 68 | # Meteorological factor: temp & humidity may slightly affect emission controls' efficacy 69 | met_factor = 1.0 + 0.002 * (ambient_temperature_C - 20) + 0.05 * (ambient_humidity - 0.5) 70 | 71 | # Nonlinear interactions and noise 72 | emissions = base_emissions * eff_factor * maint_factor * hours_factor * met_factor 73 | # Add interaction: coal plants have additional penalties when maintenance low 74 | coal_penalty = np.where(fuel_types=="coal", 1.0 + (50 - maintenance_score)/150.0, 1.0) 75 | emissions *= coal_penalty 76 | # Add random noise proportional to emissions 77 | noise = rng.normal(scale=0.05 * emissions) # 5% noise 78 | emissions = np.maximum(0.0, emissions + noise) 79 | 80 | df = pd.DataFrame({ 81 | "plant_id": plant_ids, 82 | "production_volume_tons_per_year": production_volume, 83 | "fuel_type": fuel_types, 84 | "operating_hours_per_year": operating_hours, 85 | "efficiency": efficiency, 86 | "maintenance_score": maintenance_score, 87 | "ambient_temperature_C": ambient_temperature_C, 88 | "ambient_humidity": ambient_humidity, 89 | "emissions_tons_CO2eq": np.round(emissions, 3) 90 | }) 91 | 92 | return df 93 | 94 | def build_preprocessor(): 95 | # numeric features 96 | numeric_features = ["production_volume_tons_per_year", "operating_hours_per_year", 97 | "efficiency", "maintenance_score", "ambient_temperature_C", "ambient_humidity"] 98 | numeric_transformer = Pipeline([("scaler", StandardScaler())]) 99 | 100 | # categorical features 101 | categorical_features = ["fuel_type"] 102 | categorical_transformer = Pipeline([("onehot", OneHotEncoder(drop="first"))]) 103 | 104 | preprocessor = ColumnTransformer(transformers=[ 105 | ("num", numeric_transformer, numeric_features), 106 | ("cat", categorical_transformer, categorical_features) 107 | ]) 108 | return preprocessor, numeric_features, categorical_features 109 | 110 | def train_models(df, out_dir=".", random_state=RND): 111 | os.makedirs(out_dir, exist_ok=True) 112 | os.makedirs(os.path.join(out_dir, "results"), exist_ok=True) 113 | os.makedirs(os.path.join(out_dir, "models"), exist_ok=True) 114 | os.makedirs(os.path.join(out_dir, "data"), exist_ok=True) 115 | 116 | X = df.drop(columns=["plant_id", "emissions_tons_CO2eq"]) 117 | y = df["emissions_tons_CO2eq"].values 118 | 119 | preprocessor, numeric_features, categorical_features = build_preprocessor() 120 | 121 | X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=random_state) 122 | 123 | # Define models 124 | models = { 125 | "LinearRegression": Pipeline([("pre", preprocessor), ("lr", LinearRegression())]), 126 | "Ridge": Pipeline([("pre", preprocessor), ("ridge", Ridge(alpha=1.0, random_state=random_state))]), 127 | "RandomForest": Pipeline([("pre", preprocessor), ("rf", RandomForestRegressor(n_estimators=200, random_state=random_state))]), 128 | "GradientBoosting": Pipeline([("pre", preprocessor), ("gb", GradientBoostingRegressor(n_estimators=200, learning_rate=0.05, random_state=random_state))]) 129 | } 130 | 131 | perf_records = [] 132 | trained_models = {} 133 | 134 | for name, model in models.items(): 135 | print(f"Training {name} ...") 136 | model.fit(X_train, y_train) 137 | y_pred = model.predict(X_test) 138 | rmse = mean_squared_error(y_test, y_pred, squared=False) 139 | mae = mean_absolute_error(y_test, y_pred) 140 | r2 = r2_score(y_test, y_pred) 141 | perf_records.append({"model": name, "RMSE": rmse, "MAE": mae, "R2": r2}) 142 | trained_models[name] = model 143 | 144 | # Save observed vs predicted plot 145 | plt.figure(figsize=(6,5)) 146 | plt.scatter(y_test, y_pred, alpha=0.7) 147 | mmin = min(y_test.min(), y_pred.min()) 148 | mmax = max(y_test.max(), y_pred.max()) 149 | plt.plot([mmin, mmax], [mmin, mmax], linestyle="--", color="red") 150 | plt.xlabel("Observed emissions (tons CO2-eq)") 151 | plt.ylabel("Predicted emissions (tons CO2-eq)") 152 | plt.title(f"Observed vs Predicted - {name}") 153 | plt.tight_layout() 154 | plot_path = os.path.join(out_dir, "results", f"observed_vs_pred_{name}.png") 155 | plt.savefig(plot_path, dpi=150) 156 | plt.close() 157 | 158 | # If tree-based, show feature importances 159 | if name in ["RandomForest", "GradientBoosting"]: 160 | # Need to extract feature names after preprocessing 161 | pre = model.named_steps["pre"] 162 | # numeric feature names 163 | num_feats = numeric_features 164 | # categorical feature names from OneHotEncoder 165 | ohe = pre.named_transformers_["cat"].named_steps["onehot"] 166 | cat_cols = ohe.get_feature_names_out(categorical_features).tolist() 167 | feat_names = num_feats + cat_cols 168 | importances = model.named_steps[name.lower() if name!="GradientBoosting" else "gb"].feature_importances_ 169 | fi_series = pd.Series(importances, index=feat_names).sort_values(ascending=False) 170 | fi_path = os.path.join(out_dir, "results", f"feature_importances_{name}.png") 171 | plt.figure(figsize=(8,4)) 172 | sns.barplot(x=fi_series.values, y=fi_series.index) 173 | plt.title(f"Feature importances - {name}") 174 | plt.xlabel("Importance") 175 | plt.tight_layout() 176 | plt.savefig(fi_path, dpi=150) 177 | plt.close() 178 | 179 | # save importances csv 180 | fi_series.to_csv(os.path.join(out_dir, "results", f"feature_importances_{name}.csv")) 181 | 182 | # Save model 183 | joblib.dump(model, os.path.join(out_dir, "models", f"{name}.joblib")) 184 | 185 | perf_df = pd.DataFrame(perf_records).set_index("model") 186 | perf_df.to_csv(os.path.join(out_dir, "results", "performance_table.csv")) 187 | print("\nModel performance:\n", perf_df.round(4)) 188 | 189 | # Save test predictions for the best model by RMSE 190 | best_model_name = perf_df["RMSE"].idxmin() 191 | best_model = trained_models[best_model_name] 192 | y_pred_best = best_model.predict(X_test) 193 | preds_df = X_test.copy() 194 | preds_df = preds_df.reset_index(drop=True) 195 | preds_df["observed_emissions"] = y_test 196 | preds_df["predicted_emissions"] = np.round(y_pred_best, 3) 197 | preds_df.to_csv(os.path.join(out_dir, "results", "test_predictions_best_model.csv"), index=False) 198 | 199 | return perf_df, trained_models 200 | 201 | def main(): 202 | out_dir = "industrial_emissions_project_output" 203 | os.makedirs(out_dir, exist_ok=True) 204 | 205 | print("Generating synthetic dataset...") 206 | df = generate_synthetic_industrial_data(n=500, random_state=RND) 207 | data_path = os.path.join(out_dir, "data", "industrial_emissions_synthetic.csv") 208 | os.makedirs(os.path.dirname(data_path), exist_ok=True) 209 | df.to_csv(data_path, index=False) 210 | print("Saved synthetic dataset to:", data_path) 211 | 212 | # Quick EDA plot: emissions distribution 213 | os.makedirs(os.path.join(out_dir, "results"), exist_ok=True) 214 | plt.figure(figsize=(6,4)) 215 | sns.histplot(df["emissions_tons_CO2eq"], bins=40, kde=True) 216 | plt.xlabel("Emissions (tons CO2-eq)") 217 | plt.title("Distribution of Synthetic Emissions") 218 | plt.tight_layout() 219 | plt.savefig(os.path.join(out_dir, "results", "emissions_distribution.png"), dpi=150) 220 | plt.close() 221 | 222 | # Train models and save outputs 223 | perf_df, models = train_models(df, out_dir=out_dir) 224 | 225 | print("\nAll outputs saved under:", out_dir) 226 | print("Models saved to:", os.path.join(out_dir, "models")) 227 | print("Results saved to:", os.path.join(out_dir, "results")) 228 | 229 | if __name__ == "__main__": 230 | main() 231 | --------------------------------------------------------------------------------