FeatureRank 0.1.0__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. featurerank-0.1.1/FeatureRank/GUI.py +576 -0
  2. featurerank-0.1.1/FeatureRank/__init__.py +80 -0
  3. featurerank-0.1.1/FeatureRank/__main__.py +7 -0
  4. featurerank-0.1.1/FeatureRank.egg-info/PKG-INFO +434 -0
  5. featurerank-0.1.1/FeatureRank.egg-info/SOURCES.txt +48 -0
  6. featurerank-0.1.1/FeatureRank.egg-info/dependency_links.txt +1 -0
  7. featurerank-0.1.1/FeatureRank.egg-info/entry_points.txt +2 -0
  8. featurerank-0.1.1/FeatureRank.egg-info/requires.txt +5 -0
  9. featurerank-0.1.1/FeatureRank.egg-info/top_level.txt +3 -0
  10. featurerank-0.1.1/PKG-INFO +434 -0
  11. featurerank-0.1.1/Readme.md +420 -0
  12. featurerank-0.1.1/pyproject.toml +25 -0
  13. featurerank-0.1.1/scripts/EvaluatePaperDimensionReduction.py +383 -0
  14. featurerank-0.1.1/scripts/FeatureBlockDatasetTools.py +628 -0
  15. featurerank-0.1.1/scripts/FeatureRank.py +175 -0
  16. featurerank-0.1.1/scripts/Figure_Scripts/CreateAccuracyBoxplotFromList.py +220 -0
  17. featurerank-0.1.1/scripts/Figure_Scripts/CreateClassificationFigure.py +487 -0
  18. featurerank-0.1.1/scripts/Figure_Scripts/CreateClusterClassificationSummaryFigure.py +649 -0
  19. featurerank-0.1.1/scripts/Figure_Scripts/CreateClusterFigure1.py +513 -0
  20. featurerank-0.1.1/scripts/Figure_Scripts/CreateConfusionMatrixPanel.py +118 -0
  21. featurerank-0.1.1/scripts/Figure_Scripts/CreatePrecisionRecallPanel.py +101 -0
  22. featurerank-0.1.1/scripts/Figure_Scripts/CreateRegressionFigure2.py +766 -0
  23. featurerank-0.1.1/scripts/Figure_Scripts/CreateRocPanel.py +95 -0
  24. featurerank-0.1.1/scripts/GeneratePaperDimensionReduction.py +333 -0
  25. featurerank-0.1.1/scripts/PaperDimensionReductionCommon.py +336 -0
  26. featurerank-0.1.1/scripts/RunAutoencoder.py +19 -0
  27. featurerank-0.1.1/scripts/RunBlockFeatureSelection.py +463 -0
  28. featurerank-0.1.1/scripts/RunDimensionReduction.py +145 -0
  29. featurerank-0.1.1/scripts/RunFeatureRankCV.py +689 -0
  30. featurerank-0.1.1/scripts/Simple.py +112 -0
  31. featurerank-0.1.1/scripts/__init__.py +1 -0
  32. featurerank-0.1.1/setup.cfg +4 -0
  33. featurerank-0.1.1/src/AutoencoderFeatureSelection.py +161 -0
  34. featurerank-0.1.1/src/Classification.py +1374 -0
  35. featurerank-0.1.1/src/Clustering.py +475 -0
  36. featurerank-0.1.1/src/Config.py +102 -0
  37. featurerank-0.1.1/src/DataLoader.py +262 -0
  38. featurerank-0.1.1/src/DivideCombine.py +161 -0
  39. featurerank-0.1.1/src/Experiment.py +492 -0
  40. featurerank-0.1.1/src/Models.py +108 -0
  41. featurerank-0.1.1/src/OutputPaths.py +133 -0
  42. featurerank-0.1.1/src/Preprocessing.py +305 -0
  43. featurerank-0.1.1/src/Regression.py +583 -0
  44. featurerank-0.1.1/src/Reporting.py +204 -0
  45. featurerank-0.1.1/src/Runtime.py +46 -0
  46. featurerank-0.1.1/src/Utils.py +55 -0
  47. featurerank-0.1.1/src/Workflow.py +105 -0
  48. featurerank-0.1.1/src/__init__.py +5 -0
  49. featurerank-0.1.1/tests/test_feature_rank_refactor.py +104 -0
  50. featurerank-0.1.0/.gitignore +0 -63
  51. featurerank-0.1.0/CHANGELOG.md +0 -25
  52. featurerank-0.1.0/LICENSE +0 -21
  53. featurerank-0.1.0/PKG-INFO +0 -115
  54. featurerank-0.1.0/PUBLISHING.md +0 -54
  55. featurerank-0.1.0/README.md +0 -90
  56. featurerank-0.1.0/pyproject.toml +0 -42
  57. featurerank-0.1.0/src/FeatureRank/__init__.py +0 -3
  58. featurerank-0.1.0/src/FeatureRank/basic_operator.py +0 -16
  59. featurerank-0.1.0/test.py +0 -6
  60. featurerank-0.1.0/tests/test_basic_operator.py +0 -26
@@ -0,0 +1,576 @@
1
+ """Small desktop interface for the existing FeatureRank workflows.
2
+
3
+ The window only collects user choices. Global and Divide & Combine work is
4
+ still performed by ``scripts.FeatureRank`` and the existing ``src`` modules.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import contextlib
10
+ import io
11
+ import os
12
+ from pathlib import Path
13
+ import queue
14
+ import subprocess
15
+ import sys
16
+ import threading
17
+ import time
18
+ import tkinter as tk
19
+ from tkinter import filedialog, messagebox, ttk
20
+
21
+
22
+ PERCENT_VALUES = tuple(str(value) for value in range(10, 101, 10))
23
+ BLOCK_VALUES = ("5", "10", "20")
24
+ MODE_LABELS = {
25
+ "GL": "GL — Global Feature Ranking",
26
+ "DC": "DC — Divide & Combine Feature Ranking",
27
+ }
28
+ TASK_LABELS = {
29
+ "Classification": "classification",
30
+ "Regression": "regression",
31
+ "Clustering": "clustering",
32
+ }
33
+ _WINDOW_OPENED = False
34
+
35
+
36
+ class _QueueWriter(io.TextIOBase):
37
+ """Forward workflow output to the GUI queue without changing the workflow."""
38
+
39
+ def __init__(self, messages: queue.Queue[tuple[str, object]]) -> None:
40
+ super().__init__()
41
+ self.messages = messages
42
+
43
+ def write(self, text: str) -> int:
44
+ if text:
45
+ self.messages.put(("log", text))
46
+ return len(text)
47
+
48
+ def flush(self) -> None:
49
+ return None
50
+
51
+
52
+ def _dataset_choices() -> list[str]:
53
+ """Find the repository's paired raw datasets for the dropdown."""
54
+ project_root = Path(__file__).resolve().parent.parent
55
+ directories = (Path.cwd() / "data" / "raw", project_root / "data" / "raw")
56
+ choices: list[str] = []
57
+ for directory in directories:
58
+ if not directory.exists():
59
+ continue
60
+ for data_file in sorted(directory.glob("*_data.csv")):
61
+ if data_file.is_file() and data_file.name not in choices:
62
+ choices.append(data_file.name)
63
+ return choices or ["breast_cancer_data.csv"]
64
+
65
+
66
+ def _open_folder(path: Path) -> None:
67
+ """Open a result directory using the current operating system."""
68
+ path.mkdir(parents=True, exist_ok=True)
69
+ if sys.platform == "darwin":
70
+ subprocess.Popen(["open", str(path)])
71
+ elif os.name == "nt":
72
+ os.startfile(str(path)) # type: ignore[attr-defined]
73
+ else:
74
+ subprocess.Popen(["xdg-open", str(path)])
75
+
76
+
77
+ def _tk_window_is_safe() -> bool:
78
+ """Check Tk in a child process so a native Tk crash cannot kill the app."""
79
+ probe = "import tkinter as tk; " "root = tk.Tk(); " "root.withdraw(); " "root.destroy()"
80
+ try:
81
+ result = subprocess.run(
82
+ [sys.executable, "-c", probe],
83
+ stdin=subprocess.DEVNULL,
84
+ stdout=subprocess.DEVNULL,
85
+ stderr=subprocess.DEVNULL,
86
+ timeout=5,
87
+ check=False,
88
+ )
89
+ except (OSError, subprocess.TimeoutExpired):
90
+ return False
91
+ return result.returncode == 0
92
+
93
+
94
+ class FeatureRankGUI:
95
+ """Tkinter view and controller for one FeatureRank experiment."""
96
+
97
+ def __init__(self, root: tk.Tk) -> None:
98
+ self.root = root
99
+ self.root.title("FeatureRank")
100
+ self.root.geometry("780x720")
101
+ self.root.minsize(700, 620)
102
+
103
+ self.messages: queue.Queue[tuple[str, object]] = queue.Queue()
104
+ self.worker: threading.Thread | None = None
105
+ self.last_output_dir: Path | None = None
106
+ self.block_count = 10
107
+ self.blocks_seen = 0
108
+
109
+ self.dataset_var = tk.StringVar(value=_dataset_choices()[0])
110
+ self.percent_var = tk.StringVar(value="20")
111
+ self.task_var = tk.StringVar(value="Classification")
112
+ self.mode_var = tk.StringVar(value=MODE_LABELS["GL"])
113
+ self.block_var = tk.StringVar(value="10")
114
+ self.stage_var = tk.StringVar(value="Ready")
115
+ self.status_var = tk.StringVar(value="Select a dataset and start an experiment.")
116
+ self.summary_vars = {
117
+ key: tk.StringVar(value="N/A")
118
+ for key in (
119
+ "Dataset",
120
+ "Mode",
121
+ "Feature Percentage",
122
+ "Block Count",
123
+ "Original Feature Count",
124
+ "Selected Feature Count",
125
+ "Accuracy",
126
+ "RMSE",
127
+ "Cluster RMSE",
128
+ "Execution Time",
129
+ "Output Directory",
130
+ )
131
+ }
132
+
133
+ self._configure_style()
134
+ self._build_window()
135
+
136
+ def _configure_style(self) -> None:
137
+ style = ttk.Style(self.root)
138
+ try:
139
+ style.theme_use("clam")
140
+ except tk.TclError:
141
+ pass
142
+ style.configure("Title.TLabel", font=("Helvetica", 20, "bold"), foreground="#1f2937")
143
+ style.configure("Subtitle.TLabel", font=("Helvetica", 10), foreground="#4b5563")
144
+ style.configure("Stage.TLabel", font=("Helvetica", 11, "bold"), foreground="#1f4e79")
145
+ style.configure("Run.TButton", font=("Helvetica", 11, "bold"), padding=(18, 8))
146
+
147
+ def _build_window(self) -> None:
148
+ self.root.columnconfigure(0, weight=1)
149
+ self.root.rowconfigure(3, weight=1)
150
+
151
+ header = ttk.Frame(self.root, padding=(28, 24, 28, 12))
152
+ header.grid(row=0, column=0, sticky="ew")
153
+ ttk.Label(header, text="FeatureRank", style="Title.TLabel").pack(anchor="w")
154
+ ttk.Label(
155
+ header,
156
+ text="Autoencoder-based feature ranking and selection",
157
+ style="Subtitle.TLabel",
158
+ ).pack(anchor="w", pady=(4, 0))
159
+
160
+ options = ttk.LabelFrame(self.root, text="Experiment", padding=18)
161
+ options.grid(row=1, column=0, padx=28, pady=8, sticky="ew")
162
+ options.columnconfigure(1, weight=1)
163
+
164
+ ttk.Label(options, text="Dataset").grid(row=0, column=0, sticky="w", pady=7)
165
+ self.dataset_combo = ttk.Combobox(
166
+ options, textvariable=self.dataset_var, values=_dataset_choices(), state="readonly"
167
+ )
168
+ self.dataset_combo.grid(row=0, column=1, sticky="ew", padx=(14, 8), pady=7)
169
+ ttk.Button(options, text="Browse…", command=self._browse_dataset).grid(
170
+ row=0, column=2, pady=7
171
+ )
172
+
173
+ ttk.Label(options, text="Task").grid(row=1, column=0, sticky="w", pady=7)
174
+ ttk.Combobox(
175
+ options,
176
+ textvariable=self.task_var,
177
+ values=tuple(TASK_LABELS),
178
+ state="readonly",
179
+ ).grid(row=1, column=1, sticky="ew", padx=(14, 8), pady=7)
180
+
181
+ ttk.Label(options, text="Feature Percent").grid(row=2, column=0, sticky="w", pady=7)
182
+ ttk.Combobox(
183
+ options,
184
+ textvariable=self.percent_var,
185
+ values=PERCENT_VALUES,
186
+ state="readonly",
187
+ width=12,
188
+ ).grid(row=2, column=1, sticky="w", padx=(14, 8), pady=7)
189
+
190
+ ttk.Label(options, text="Mode").grid(row=3, column=0, sticky="w", pady=7)
191
+ self.mode_combo = ttk.Combobox(
192
+ options,
193
+ textvariable=self.mode_var,
194
+ values=tuple(MODE_LABELS.values()),
195
+ state="readonly",
196
+ )
197
+ self.mode_combo.grid(row=3, column=1, sticky="ew", padx=(14, 8), pady=7)
198
+ self.mode_combo.bind("<<ComboboxSelected>>", self._mode_changed)
199
+
200
+ ttk.Label(options, text="Block Count").grid(row=4, column=0, sticky="w", pady=7)
201
+ self.block_combo = ttk.Combobox(
202
+ options, textvariable=self.block_var, values=BLOCK_VALUES, state="disabled", width=12
203
+ )
204
+ self.block_combo.grid(row=4, column=1, sticky="w", padx=(14, 8), pady=7)
205
+
206
+ action_row = ttk.Frame(self.root, padding=(28, 8, 28, 8))
207
+ action_row.grid(row=2, column=0, sticky="ew")
208
+ action_row.columnconfigure(1, weight=1)
209
+ self.start_button = ttk.Button(
210
+ action_row, text="START", style="Run.TButton", command=self._start
211
+ )
212
+ self.start_button.grid(row=0, column=0, sticky="w")
213
+ ttk.Label(action_row, textvariable=self.stage_var, style="Stage.TLabel").grid(
214
+ row=0, column=1, sticky="e", padx=(12, 0)
215
+ )
216
+
217
+ progress_frame = ttk.Frame(self.root, padding=(28, 0, 28, 12))
218
+ progress_frame.grid(row=3, column=0, sticky="nsew")
219
+ progress_frame.columnconfigure(0, weight=1)
220
+ progress_frame.rowconfigure(2, weight=1)
221
+ self.progress = ttk.Progressbar(progress_frame, mode="determinate", maximum=100)
222
+ self.progress.grid(row=0, column=0, sticky="ew")
223
+ ttk.Label(progress_frame, textvariable=self.status_var, style="Subtitle.TLabel").grid(
224
+ row=1, column=0, sticky="w", pady=(6, 8)
225
+ )
226
+
227
+ log_frame = ttk.LabelFrame(progress_frame, text="Log / Output", padding=8)
228
+ log_frame.grid(row=2, column=0, sticky="nsew")
229
+ log_frame.columnconfigure(0, weight=1)
230
+ log_frame.rowconfigure(0, weight=1)
231
+ self.log_text = tk.Text(
232
+ log_frame,
233
+ height=13,
234
+ wrap="word",
235
+ state="disabled",
236
+ background="#f8fafc",
237
+ foreground="#1f2937",
238
+ relief="flat",
239
+ padx=8,
240
+ pady=8,
241
+ )
242
+ self.log_text.grid(row=0, column=0, sticky="nsew")
243
+ scrollbar = ttk.Scrollbar(log_frame, orient="vertical", command=self.log_text.yview)
244
+ scrollbar.grid(row=0, column=1, sticky="ns")
245
+ self.log_text.configure(yscrollcommand=scrollbar.set)
246
+
247
+ summary = ttk.LabelFrame(self.root, text="Summary", padding=14)
248
+ summary.grid(row=4, column=0, padx=28, pady=(0, 10), sticky="ew")
249
+ summary.columnconfigure(1, weight=1)
250
+ for row, (key, value) in enumerate(self.summary_vars.items()):
251
+ ttk.Label(summary, text=key).grid(row=row // 2, column=(row % 2) * 2, sticky="w")
252
+ ttk.Label(summary, textvariable=value, foreground="#1f4e79").grid(
253
+ row=row // 2, column=(row % 2) * 2 + 1, sticky="w", padx=(8, 26), pady=2
254
+ )
255
+
256
+ self.open_button = ttk.Button(
257
+ self.root, text="Open Results Folder", command=self._open_results, state="disabled"
258
+ )
259
+ self.open_button.grid(row=5, column=0, padx=28, pady=(0, 18), sticky="w")
260
+
261
+ def _browse_dataset(self) -> None:
262
+ selected = filedialog.askopenfilename(
263
+ title="Select dataset",
264
+ filetypes=[("CSV files", "*.csv"), ("Text files", "*.txt"), ("All files", "*.*")],
265
+ )
266
+ if selected:
267
+ self.dataset_var.set(selected)
268
+
269
+ def _mode_changed(self, _event: object = None) -> None:
270
+ is_dc = self.mode_var.get().startswith("DC")
271
+ self.block_combo.configure(state="readonly" if is_dc else "disabled")
272
+
273
+ def _start(self) -> None:
274
+ dataset = self.dataset_var.get().strip()
275
+ if not dataset:
276
+ messagebox.showerror("FeatureRank", "Please select a dataset.")
277
+ return
278
+ try:
279
+ percent = float(self.percent_var.get())
280
+ if not 0 < percent <= 100:
281
+ raise ValueError
282
+ except ValueError:
283
+ messagebox.showerror("FeatureRank", "Feature Percent must be between 10 and 100.")
284
+ return
285
+
286
+ mode = "dc" if self.mode_var.get().startswith("DC") else "global"
287
+ task = TASK_LABELS[self.task_var.get()]
288
+ block_count = int(self.block_var.get()) if mode == "dc" else 10
289
+ self.block_count = block_count
290
+ self.blocks_seen = 0
291
+ self.last_output_dir = None
292
+ self.open_button.configure(state="disabled")
293
+ self.start_button.configure(state="disabled")
294
+ self.progress.configure(value=2)
295
+ self.stage_var.set("Loading Dataset")
296
+ self.status_var.set("The experiment is running. You can follow progress in the log.")
297
+ self._clear_log()
298
+
299
+ self.worker = threading.Thread(
300
+ target=self._run_experiment,
301
+ args=(dataset, percent, task, mode, block_count),
302
+ daemon=True,
303
+ )
304
+ self.worker.start()
305
+ self.root.after(100, self._poll_messages)
306
+
307
+ def _run_experiment(
308
+ self, dataset: str, percent: float, task: str, mode: str, block_count: int
309
+ ) -> None:
310
+ started_at = time.perf_counter()
311
+ writer = _QueueWriter(self.messages)
312
+ try:
313
+ # These imports intentionally stay inside the worker. Importing
314
+ # FeatureRank remains quick, and all computation reuses the CLI
315
+ # orchestration and task implementations already in the project.
316
+ from scripts.FeatureRank import metric_name_for_task, run_workflow
317
+ from src.Config import ExperimentConfig
318
+ from src.OutputPaths import metric_runs_path
319
+
320
+ config = ExperimentConfig(
321
+ dataset_name=dataset,
322
+ task=task,
323
+ feature_percent=percent,
324
+ random_state=42,
325
+ encoding_dim=8,
326
+ target_column="target",
327
+ id_column="ID",
328
+ cluster_k=None,
329
+ save_details=False,
330
+ )
331
+ metric_path = metric_runs_path(config.task, Path(dataset).stem, percent)
332
+ with contextlib.redirect_stdout(writer), contextlib.redirect_stderr(writer):
333
+ values, average = run_workflow(
334
+ config=config,
335
+ mode=mode,
336
+ repeat_runs=1,
337
+ metric_name=metric_name_for_task(config.task),
338
+ metric_path=metric_path,
339
+ block_count=block_count,
340
+ )
341
+ summary = self._read_summary(
342
+ config,
343
+ mode,
344
+ block_count,
345
+ average,
346
+ values,
347
+ time.perf_counter() - started_at,
348
+ )
349
+ self.messages.put(("done", summary))
350
+ except Exception as error: # show a useful GUI message instead of a traceback window
351
+ self.messages.put(("error", str(error)))
352
+
353
+ def _read_summary(
354
+ self,
355
+ config: object,
356
+ mode: str,
357
+ block_count: int,
358
+ average: float,
359
+ values: list[float],
360
+ elapsed: float,
361
+ ) -> dict[str, str]:
362
+ import json
363
+
364
+ from src.OutputPaths import format_feature_percent_tag, task_output_dir
365
+
366
+ dataset_name = str(config.dataset_name)
367
+ task = str(config.task)
368
+ tag = format_feature_percent_tag(float(config.feature_percent))
369
+ folder = Path(dataset_name).stem
370
+ if mode == "dc":
371
+ from scripts.FeatureBlockDatasetTools import dataset_base_name
372
+
373
+ folder = f"{dataset_base_name(dataset_name)}_selected_features_combined_data"
374
+ metrics_dir = task_output_dir(task, folder) / "metrics"
375
+ metric_filename = (
376
+ f"top_{tag}_cluster_metrics.json"
377
+ if task == "clustering"
378
+ else f"top_{tag}_test_metrics.json"
379
+ )
380
+ metric_path = metrics_dir / metric_filename
381
+ if not metric_path.exists():
382
+ candidates = sorted(
383
+ (
384
+ metrics_dir.parent.parent.rglob(f"top_{tag}*_metrics.json")
385
+ if metrics_dir.parent.parent.exists()
386
+ else []
387
+ ),
388
+ key=lambda item: item.stat().st_mtime,
389
+ )
390
+ metric_path = candidates[-1] if candidates else metric_path
391
+
392
+ metrics: dict[str, object] = {}
393
+ if metric_path.exists():
394
+ metrics = json.loads(metric_path.read_text(encoding="utf-8"))
395
+ selected_count = metrics.get("selected_feature_count", "N/A")
396
+ original_count = metrics.get("original_feature_count", "N/A")
397
+ if original_count == "N/A":
398
+ original_count = self._count_features(config)
399
+ accuracy = metrics.get("test_accuracy", average) if task == "classification" else "N/A"
400
+ rmse = metrics.get("regression_rmse", average) if task == "regression" else "N/A"
401
+ cluster_rmse = metrics.get("cluster_rmse", average) if task == "clustering" else "N/A"
402
+
403
+ return {
404
+ "Dataset": Path(dataset_name).name,
405
+ "Mode": "DC" if mode == "dc" else "GL",
406
+ "Feature Percentage": f"{float(config.feature_percent):g}%",
407
+ "Block Count": str(block_count) if mode == "dc" else "N/A",
408
+ "Original Feature Count": str(original_count),
409
+ "Selected Feature Count": str(selected_count),
410
+ "Accuracy": f"{float(accuracy):.6f}" if accuracy != "N/A" else "N/A",
411
+ "RMSE": f"{float(rmse):.6f}" if rmse != "N/A" else "N/A",
412
+ "Cluster RMSE": (f"{float(cluster_rmse):.6f}" if cluster_rmse != "N/A" else "N/A"),
413
+ "Execution Time": f"{float(metrics.get('elapsed_seconds', elapsed)):.2f} s",
414
+ "Output Directory": str(metric_path.parent.parent) if metric_path.exists() else "N/A",
415
+ }
416
+
417
+ @staticmethod
418
+ def _count_features(config: object) -> str:
419
+ try:
420
+ from src.DataLoader import load_data
421
+
422
+ frame = load_data(config.dataset_name, target_column=config.target_column)
423
+ excluded = {config.target_column}
424
+ if config.id_column:
425
+ excluded.add(config.id_column)
426
+ return str(len([column for column in frame.columns if column not in excluded]))
427
+ except Exception:
428
+ return "N/A"
429
+
430
+ def _poll_messages(self) -> None:
431
+ while True:
432
+ try:
433
+ kind, payload = self.messages.get_nowait()
434
+ except queue.Empty:
435
+ break
436
+ if kind == "log":
437
+ self._handle_log(str(payload))
438
+ elif kind == "done":
439
+ self._completed(payload) # type: ignore[arg-type]
440
+ elif kind == "error":
441
+ self._failed(str(payload))
442
+
443
+ if self.worker and self.worker.is_alive():
444
+ self.root.after(100, self._poll_messages)
445
+
446
+ def _handle_log(self, text: str) -> None:
447
+ for line in text.splitlines():
448
+ line = line.strip()
449
+ if not line or self._is_debug_line(line):
450
+ continue
451
+ self._append_log(line)
452
+ self._update_progress(line)
453
+
454
+ @staticmethod
455
+ def _is_debug_line(line: str) -> bool:
456
+ lower = line.lower()
457
+ return any(
458
+ marker in lower
459
+ for marker in ("onednn", "cuda error", "cudnn", "cublas", "tensorflow/core")
460
+ )
461
+
462
+ def _update_progress(self, line: str) -> None:
463
+ lower = line.lower()
464
+ if "veri yukleniyor" in lower or "loading" in lower:
465
+ self._set_progress("Loading Dataset", 10)
466
+ elif "[dc] blok" in lower:
467
+ self.blocks_seen += 1
468
+ progress = 15 + int(50 * self.blocks_seen / max(self.block_count, 1))
469
+ self._set_progress(f"Processing Block {self.blocks_seen}/{self.block_count}", progress)
470
+ elif "autoencoder" in lower or "feature" in lower and "sec" in lower:
471
+ self._set_progress("Ranking Features", 55)
472
+ elif "birles" in lower or "combine" in lower:
473
+ self._set_progress("Combining Features", 75)
474
+ elif "egitim" in lower or "training" in lower or "classifier" in lower:
475
+ self._set_progress("Training Model", 85)
476
+ elif "metrik" in lower or "metric" in lower or "tamamlandi" in lower:
477
+ self._set_progress("Calculating Results", 95)
478
+
479
+ def _set_progress(self, stage: str, value: int) -> None:
480
+ self.stage_var.set(stage)
481
+ self.progress.configure(value=min(value, 99))
482
+
483
+ def _completed(self, summary: dict[str, str]) -> None:
484
+ for key, value in summary.items():
485
+ self.summary_vars[key].set(value)
486
+ output = summary.get("Output Directory", "N/A")
487
+ if output != "N/A":
488
+ self.last_output_dir = Path(output)
489
+ self.open_button.configure(state="normal")
490
+ self.progress.configure(value=100)
491
+ self.stage_var.set("Completed")
492
+ self.status_var.set("FeatureRank completed successfully.")
493
+ self.start_button.configure(state="normal")
494
+
495
+ def _failed(self, error: str) -> None:
496
+ self.progress.configure(value=0)
497
+ self.stage_var.set("Failed")
498
+ self.status_var.set("The experiment could not be completed.")
499
+ self._append_log(f"ERROR: {error}")
500
+ self.start_button.configure(state="normal")
501
+ messagebox.showerror("FeatureRank", error)
502
+
503
+ def _clear_log(self) -> None:
504
+ self.log_text.configure(state="normal")
505
+ self.log_text.delete("1.0", tk.END)
506
+ self.log_text.configure(state="disabled")
507
+
508
+ def _append_log(self, line: str) -> None:
509
+ self.log_text.configure(state="normal")
510
+ self.log_text.insert(tk.END, line + "\n")
511
+ self.log_text.see(tk.END)
512
+ self.log_text.configure(state="disabled")
513
+
514
+ def _open_results(self) -> None:
515
+ if self.last_output_dir is not None:
516
+ _open_folder(self.last_output_dir)
517
+
518
+
519
+ def Launch() -> FeatureRankGUI | None:
520
+ """Open the FeatureRank desktop application.
521
+
522
+ ``None`` is returned in headless environments so importing the package
523
+ does not crash notebooks, documentation builders, or automated tests.
524
+ """
525
+ global _WINDOW_OPENED
526
+ if _WINDOW_OPENED:
527
+ return None
528
+ # Codex and other CI runners may expose Tkinter while lacking a window
529
+ # server. Calling ``Tk()`` on macOS in that state can abort the process,
530
+ # so leave imports safe in those runners.
531
+ if os.environ.get("CODEX_CI") == "1":
532
+ return None
533
+
534
+ # Anaconda's macOS interpreter can crash inside Cocoa/Tk when a window is
535
+ # created from an interactive ``python`` process. ``pythonw`` owns the
536
+ # application event loop, so launch the GUI there and keep the import
537
+ # process safe. The child sets a guard and enters this function directly.
538
+ if sys.platform == "darwin" and os.environ.get("FEATURERANK_GUI_CHILD") != "1":
539
+ pythonw = Path(sys.executable).with_name("pythonw")
540
+ if pythonw.exists() and _tk_window_is_safe():
541
+ child_environment = os.environ.copy()
542
+ child_environment["FEATURERANK_NO_AUTO_GUI"] = "1"
543
+ child_environment["FEATURERANK_GUI_CHILD"] = "1"
544
+ try:
545
+ subprocess.Popen(
546
+ [
547
+ str(pythonw),
548
+ "-c",
549
+ "from FeatureRank.GUI import Launch; Launch()",
550
+ ],
551
+ env=child_environment,
552
+ start_new_session=True,
553
+ )
554
+ except OSError:
555
+ pass
556
+ else:
557
+ _WINDOW_OPENED = True
558
+ return None
559
+
560
+ if sys.platform not in {"darwin", "win32"} and not (
561
+ os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")
562
+ ):
563
+ return None
564
+ if not _tk_window_is_safe():
565
+ return None
566
+ try:
567
+ root = tk.Tk()
568
+ except tk.TclError:
569
+ return None
570
+ _WINDOW_OPENED = True
571
+ application = FeatureRankGUI(root)
572
+ root.mainloop()
573
+ return application
574
+
575
+
576
+ __all__ = ["FeatureRankGUI", "Launch"]
@@ -0,0 +1,80 @@
1
+ """Public Python entry point for the FeatureRank project.
2
+
3
+ On a normal desktop import the GUI opens automatically. The explicit
4
+ ``Launch`` function remains available for applications that want to control
5
+ when the window is shown.
6
+ """
7
+
8
+ import os
9
+ from pathlib import Path
10
+ import sys
11
+
12
+ from .GUI import Launch
13
+
14
+ __version__ = "0.1.1"
15
+
16
+
17
+ def _run_cli(arguments: list[str] | None = None) -> None:
18
+ """Load the CLI only when a workflow is actually started."""
19
+ from scripts.FeatureRank import main as run_cli
20
+
21
+ run_cli(arguments)
22
+
23
+
24
+ def run(
25
+ dataset_name: str,
26
+ feature_percent: float,
27
+ task: str = "classification",
28
+ mode: str = "global",
29
+ block_count: int = 10,
30
+ random_state: int | None = 42,
31
+ ) -> None:
32
+ """Start one FeatureRank experiment from Python code.
33
+
34
+ ``dataset_name`` and ``feature_percent`` are the only required values, so
35
+ a graphical interface can call this function directly. The remaining
36
+ arguments mirror the small set of choices exposed by the command line.
37
+ """
38
+ mode_name = mode.lower().strip()
39
+ if mode_name not in {"global", "dc"}:
40
+ raise ValueError("mode 'global' veya 'dc' olmali.")
41
+
42
+ arguments = [
43
+ "--dataset-name",
44
+ str(dataset_name),
45
+ "--task",
46
+ str(task),
47
+ "--feature-percent",
48
+ str(feature_percent),
49
+ "--random-state",
50
+ "none" if random_state is None else str(random_state),
51
+ ]
52
+ arguments.append("--dc" if mode_name == "dc" else "--global")
53
+ if mode_name == "dc":
54
+ arguments.extend(["--block-count", str(block_count)])
55
+
56
+ _run_cli(arguments)
57
+
58
+
59
+ def main() -> None:
60
+ """Entry point used by the installed ``FeatureRank`` command."""
61
+ Launch()
62
+
63
+
64
+ def _should_auto_launch() -> bool:
65
+ """Avoid surprise windows in tests and in ``python -m`` startup."""
66
+ if os.environ.get("FEATURERANK_NO_GUI", "").lower() in {"1", "true", "yes"}:
67
+ return False
68
+ if os.environ.get("FEATURERANK_NO_AUTO_GUI", "").lower() in {"1", "true", "yes"}:
69
+ return False
70
+ if "pytest" in sys.modules or os.environ.get("PYTEST_CURRENT_TEST"):
71
+ return False
72
+ command_name = Path(sys.argv[0]).name if sys.argv else ""
73
+ return command_name not in {"FeatureRank", "FeatureRank.exe", "__main__.py"}
74
+
75
+
76
+ if _should_auto_launch():
77
+ Launch()
78
+
79
+
80
+ __all__ = ["Launch", "main", "run", "__version__"]
@@ -0,0 +1,7 @@
1
+ """Allow ``python -m FeatureRank`` to open the desktop application."""
2
+
3
+ from . import Launch
4
+
5
+
6
+ if __name__ == "__main__":
7
+ Launch()