FeatureRank 0.1.2__tar.gz → 0.1.4__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 (50) hide show
  1. {featurerank-0.1.2 → featurerank-0.1.4}/FeatureRank/GUI.py +181 -43
  2. {featurerank-0.1.2 → featurerank-0.1.4}/FeatureRank/__init__.py +1 -1
  3. {featurerank-0.1.2 → featurerank-0.1.4/FeatureRank.egg-info}/PKG-INFO +75 -30
  4. featurerank-0.1.4/FeatureRank.egg-info/requires.txt +11 -0
  5. featurerank-0.1.2/README.md → featurerank-0.1.4/PKG-INFO +87 -27
  6. featurerank-0.1.2/FeatureRank.egg-info/PKG-INFO → featurerank-0.1.4/README.md +71 -42
  7. {featurerank-0.1.2 → featurerank-0.1.4}/pyproject.toml +4 -3
  8. featurerank-0.1.2/FeatureRank.egg-info/requires.txt +0 -6
  9. {featurerank-0.1.2 → featurerank-0.1.4}/FeatureRank/__main__.py +0 -0
  10. {featurerank-0.1.2 → featurerank-0.1.4}/FeatureRank.egg-info/SOURCES.txt +0 -0
  11. {featurerank-0.1.2 → featurerank-0.1.4}/FeatureRank.egg-info/dependency_links.txt +0 -0
  12. {featurerank-0.1.2 → featurerank-0.1.4}/FeatureRank.egg-info/entry_points.txt +0 -0
  13. {featurerank-0.1.2 → featurerank-0.1.4}/FeatureRank.egg-info/top_level.txt +0 -0
  14. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/EvaluatePaperDimensionReduction.py +0 -0
  15. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/FeatureBlockDatasetTools.py +0 -0
  16. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/FeatureRank.py +0 -0
  17. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/Figure_Scripts/CreateAccuracyBoxplotFromList.py +0 -0
  18. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/Figure_Scripts/CreateClassificationFigure.py +0 -0
  19. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/Figure_Scripts/CreateClusterClassificationSummaryFigure.py +0 -0
  20. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/Figure_Scripts/CreateClusterFigure1.py +0 -0
  21. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/Figure_Scripts/CreateConfusionMatrixPanel.py +0 -0
  22. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/Figure_Scripts/CreatePrecisionRecallPanel.py +0 -0
  23. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/Figure_Scripts/CreateRegressionFigure2.py +0 -0
  24. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/Figure_Scripts/CreateRocPanel.py +0 -0
  25. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/GeneratePaperDimensionReduction.py +0 -0
  26. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/PaperDimensionReductionCommon.py +0 -0
  27. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/RunAutoencoder.py +0 -0
  28. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/RunBlockFeatureSelection.py +0 -0
  29. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/RunDimensionReduction.py +0 -0
  30. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/RunFeatureRankCV.py +0 -0
  31. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/Simple.py +0 -0
  32. {featurerank-0.1.2 → featurerank-0.1.4}/scripts/__init__.py +0 -0
  33. {featurerank-0.1.2 → featurerank-0.1.4}/setup.cfg +0 -0
  34. {featurerank-0.1.2 → featurerank-0.1.4}/src/AutoencoderFeatureSelection.py +0 -0
  35. {featurerank-0.1.2 → featurerank-0.1.4}/src/Classification.py +0 -0
  36. {featurerank-0.1.2 → featurerank-0.1.4}/src/Clustering.py +0 -0
  37. {featurerank-0.1.2 → featurerank-0.1.4}/src/Config.py +0 -0
  38. {featurerank-0.1.2 → featurerank-0.1.4}/src/DataLoader.py +0 -0
  39. {featurerank-0.1.2 → featurerank-0.1.4}/src/DivideCombine.py +0 -0
  40. {featurerank-0.1.2 → featurerank-0.1.4}/src/Experiment.py +0 -0
  41. {featurerank-0.1.2 → featurerank-0.1.4}/src/Models.py +0 -0
  42. {featurerank-0.1.2 → featurerank-0.1.4}/src/OutputPaths.py +0 -0
  43. {featurerank-0.1.2 → featurerank-0.1.4}/src/Preprocessing.py +0 -0
  44. {featurerank-0.1.2 → featurerank-0.1.4}/src/Regression.py +0 -0
  45. {featurerank-0.1.2 → featurerank-0.1.4}/src/Reporting.py +0 -0
  46. {featurerank-0.1.2 → featurerank-0.1.4}/src/Runtime.py +0 -0
  47. {featurerank-0.1.2 → featurerank-0.1.4}/src/Utils.py +0 -0
  48. {featurerank-0.1.2 → featurerank-0.1.4}/src/Workflow.py +0 -0
  49. {featurerank-0.1.2 → featurerank-0.1.4}/src/__init__.py +0 -0
  50. {featurerank-0.1.2 → featurerank-0.1.4}/tests/test_feature_rank_refactor.py +0 -0
@@ -19,6 +19,7 @@ from tkinter import filedialog, messagebox, ttk
19
19
 
20
20
  PERCENT_VALUES = tuple(str(value) for value in range(10, 101, 10))
21
21
  BLOCK_VALUES = ("5", "10", "20")
22
+ NO_DATASET_LABEL = "No Dataset"
22
23
  MODE_LABELS = {
23
24
  "GL": "GL — Global Feature Ranking",
24
25
  "DC": "DC — Divide & Combine Feature Ranking",
@@ -40,9 +41,42 @@ def _dataset_choices() -> list[str]:
40
41
  if not directory.exists():
41
42
  continue
42
43
  for data_file in sorted(directory.glob("*_data.csv")):
43
- if data_file.is_file() and data_file.name not in choices:
44
- choices.append(data_file.name)
45
- return choices or ["breast_cancer_data.csv"]
44
+ absolute_path = str(data_file.resolve())
45
+ if data_file.is_file() and absolute_path not in choices:
46
+ choices.append(absolute_path)
47
+ return choices or [NO_DATASET_LABEL]
48
+
49
+
50
+ def _resolve_dataset_path(dataset: str) -> str:
51
+ """Use an absolute path when the selected file exists locally."""
52
+ requested = Path(dataset).expanduser()
53
+ if requested.is_file():
54
+ return str(requested.resolve())
55
+
56
+ project_root = Path(__file__).resolve().parent.parent
57
+ candidates = (
58
+ Path.cwd() / requested,
59
+ Path.cwd() / "data" / "raw" / requested,
60
+ project_root / requested,
61
+ project_root / "data" / "raw" / requested,
62
+ )
63
+ for candidate in candidates:
64
+ if candidate.is_file():
65
+ return str(candidate.resolve())
66
+ return dataset
67
+
68
+
69
+ def _worker_python() -> Path:
70
+ """Find a project virtual-environment interpreter before the system one."""
71
+ project_root = Path(__file__).resolve().parent.parent
72
+ executable_name = "python.exe" if os.name == "nt" else "python"
73
+ candidates = (
74
+ Path.cwd() / ".venv" / ("Scripts" if os.name == "nt" else "bin") / executable_name,
75
+ project_root / ".venv" / ("Scripts" if os.name == "nt" else "bin") / executable_name,
76
+ Path(sys.executable).with_name("python"),
77
+ Path(sys.executable),
78
+ )
79
+ return next((candidate for candidate in candidates if candidate.is_file()), candidates[-1])
46
80
 
47
81
 
48
82
  def _open_folder(path: Path) -> None:
@@ -79,16 +113,27 @@ class FeatureRankGUI:
79
113
  def __init__(self, root: tk.Tk) -> None:
80
114
  self.root = root
81
115
  self.root.title("FeatureRank")
82
- self.root.geometry("780x720")
83
- self.root.minsize(700, 620)
116
+ # Keep the first view compact; the log can still grow when resized.
117
+ self.root.geometry("760x680")
118
+ self.root.minsize(680, 600)
84
119
 
85
120
  self.messages: queue.Queue[tuple[str, object]] = queue.Queue()
86
121
  self.worker: threading.Thread | None = None
87
122
  self.last_output_dir: Path | None = None
88
123
  self.block_count = 10
89
124
  self.blocks_seen = 0
125
+ self.progress_value = 0.0
126
+ self.progress_started_at: float | None = None
127
+ self.progress_job: str | None = None
128
+ self.progress_running = False
129
+
130
+ dataset_paths = _dataset_choices()
131
+ self.dataset_paths = {
132
+ Path(path).name: path for path in dataset_paths if path != NO_DATASET_LABEL
133
+ }
134
+ self.dataset_labels = [NO_DATASET_LABEL, *self.dataset_paths]
90
135
 
91
- self.dataset_var = tk.StringVar(value=_dataset_choices()[0])
136
+ self.dataset_var = tk.StringVar(value=NO_DATASET_LABEL)
92
137
  self.percent_var = tk.StringVar(value="20")
93
138
  self.task_var = tk.StringVar(value="Classification")
94
139
  self.mode_var = tk.StringVar(value=MODE_LABELS["GL"])
@@ -121,16 +166,56 @@ class FeatureRankGUI:
121
166
  style.theme_use("clam")
122
167
  except tk.TclError:
123
168
  pass
124
- style.configure("Title.TLabel", font=("Helvetica", 20, "bold"), foreground="#1f2937")
125
- style.configure("Subtitle.TLabel", font=("Helvetica", 10), foreground="#4b5563")
126
- style.configure("Stage.TLabel", font=("Helvetica", 11, "bold"), foreground="#1f4e79")
127
- style.configure("Run.TButton", font=("Helvetica", 11, "bold"), padding=(18, 8))
169
+ # A restrained palette gives the form a clean surface on all platforms.
170
+ self.root.configure(background="#f4f6f8")
171
+ style.configure("TFrame", background="#f4f6f8")
172
+ style.configure("TLabel", background="#ffffff", foreground="#1f2937")
173
+ style.configure("TLabelframe", background="#ffffff")
174
+ style.configure("TLabelframe.Label", background="#ffffff", foreground="#1f2937")
175
+ style.configure(
176
+ "Title.TLabel",
177
+ background="#f4f6f8",
178
+ font=("Helvetica", 20, "bold"),
179
+ foreground="#1f2937",
180
+ )
181
+ style.configure(
182
+ "Subtitle.TLabel",
183
+ background="#f4f6f8",
184
+ font=("Helvetica", 10),
185
+ foreground="#4b5563",
186
+ )
187
+ style.configure(
188
+ "Stage.TLabel",
189
+ background="#f4f6f8",
190
+ font=("Helvetica", 11, "bold"),
191
+ foreground="#1f4e79",
192
+ )
193
+ style.configure(
194
+ "Run.TButton",
195
+ background="#1f4e79",
196
+ foreground="#ffffff",
197
+ font=("Helvetica", 11, "bold"),
198
+ padding=(16, 6),
199
+ )
200
+ style.map(
201
+ "Run.TButton",
202
+ background=[("active", "#173a5c"), ("disabled", "#b9c3cf")],
203
+ foreground=[("disabled", "#eef2f6")],
204
+ )
205
+ style.configure(
206
+ "Horizontal.TProgressbar",
207
+ background="#1f4e79",
208
+ troughcolor="#e5e7eb",
209
+ bordercolor="#d1d5db",
210
+ lightcolor="#1f4e79",
211
+ darkcolor="#1f4e79",
212
+ )
128
213
 
129
214
  def _build_window(self) -> None:
130
215
  self.root.columnconfigure(0, weight=1)
131
216
  self.root.rowconfigure(3, weight=1)
132
217
 
133
- header = ttk.Frame(self.root, padding=(28, 24, 28, 12))
218
+ header = ttk.Frame(self.root, padding=(24, 18, 24, 8))
134
219
  header.grid(row=0, column=0, sticky="ew")
135
220
  ttk.Label(header, text="FeatureRank", style="Title.TLabel").pack(anchor="w")
136
221
  ttk.Label(
@@ -139,53 +224,55 @@ class FeatureRankGUI:
139
224
  style="Subtitle.TLabel",
140
225
  ).pack(anchor="w", pady=(4, 0))
141
226
 
142
- options = ttk.LabelFrame(self.root, text="Experiment", padding=18)
143
- options.grid(row=1, column=0, padx=28, pady=8, sticky="ew")
227
+ options = ttk.LabelFrame(self.root, text="Experiment", padding=14)
228
+ options.grid(row=1, column=0, padx=24, pady=6, sticky="ew")
144
229
  options.columnconfigure(1, weight=1)
145
230
 
146
- ttk.Label(options, text="Dataset").grid(row=0, column=0, sticky="w", pady=7)
231
+ ttk.Label(options, text="Dataset").grid(row=0, column=0, sticky="w", pady=5)
147
232
  self.dataset_combo = ttk.Combobox(
148
- options, textvariable=self.dataset_var, values=_dataset_choices(), state="readonly"
233
+ options, textvariable=self.dataset_var, values=self.dataset_labels, state="readonly"
149
234
  )
150
- self.dataset_combo.grid(row=0, column=1, sticky="ew", padx=(14, 8), pady=7)
235
+ self.dataset_combo.grid(row=0, column=1, sticky="ew", padx=(10, 6), pady=5)
151
236
  ttk.Button(options, text="Browse…", command=self._browse_dataset).grid(
152
- row=0, column=2, pady=7
237
+ row=0, column=2, pady=5
153
238
  )
154
239
 
155
- ttk.Label(options, text="Task").grid(row=1, column=0, sticky="w", pady=7)
240
+ ttk.Label(options, text="Task").grid(row=1, column=0, sticky="w", pady=5)
156
241
  ttk.Combobox(
157
242
  options,
158
243
  textvariable=self.task_var,
159
244
  values=tuple(TASK_LABELS),
160
245
  state="readonly",
161
- ).grid(row=1, column=1, sticky="ew", padx=(14, 8), pady=7)
246
+ ).grid(row=1, column=1, sticky="ew", padx=(10, 6), pady=5)
162
247
 
163
- ttk.Label(options, text="Feature Percent").grid(row=2, column=0, sticky="w", pady=7)
248
+ ttk.Label(options, text="Feature Percent").grid(row=2, column=0, sticky="w", pady=5)
164
249
  ttk.Combobox(
165
250
  options,
166
251
  textvariable=self.percent_var,
167
252
  values=PERCENT_VALUES,
168
253
  state="readonly",
169
254
  width=12,
170
- ).grid(row=2, column=1, sticky="w", padx=(14, 8), pady=7)
255
+ ).grid(row=2, column=1, sticky="w", padx=(10, 6), pady=5)
171
256
 
172
- ttk.Label(options, text="Mode").grid(row=3, column=0, sticky="w", pady=7)
257
+ ttk.Label(options, text="Mode").grid(row=3, column=0, sticky="w", pady=5)
173
258
  self.mode_combo = ttk.Combobox(
174
259
  options,
175
260
  textvariable=self.mode_var,
176
261
  values=tuple(MODE_LABELS.values()),
177
262
  state="readonly",
178
263
  )
179
- self.mode_combo.grid(row=3, column=1, sticky="ew", padx=(14, 8), pady=7)
264
+ self.mode_combo.grid(row=3, column=1, sticky="ew", padx=(10, 6), pady=5)
180
265
  self.mode_combo.bind("<<ComboboxSelected>>", self._mode_changed)
181
266
 
182
- ttk.Label(options, text="Block Count").grid(row=4, column=0, sticky="w", pady=7)
267
+ ttk.Label(options, text="Block Count").grid(row=4, column=0, sticky="w", pady=5)
183
268
  self.block_combo = ttk.Combobox(
184
269
  options, textvariable=self.block_var, values=BLOCK_VALUES, state="disabled", width=12
185
270
  )
186
- self.block_combo.grid(row=4, column=1, sticky="w", padx=(14, 8), pady=7)
271
+ self.block_combo.grid(row=4, column=1, sticky="w", padx=(10, 6), pady=5)
187
272
 
188
- action_row = ttk.Frame(self.root, padding=(28, 8, 28, 8))
273
+ # Keep the run controls in a shallow toolbar so START is not surrounded
274
+ # by a large empty band.
275
+ action_row = ttk.Frame(self.root, padding=(24, 2, 24, 2))
189
276
  action_row.grid(row=2, column=0, sticky="ew")
190
277
  action_row.columnconfigure(1, weight=1)
191
278
  self.start_button = ttk.Button(
@@ -196,14 +283,14 @@ class FeatureRankGUI:
196
283
  row=0, column=1, sticky="e", padx=(12, 0)
197
284
  )
198
285
 
199
- progress_frame = ttk.Frame(self.root, padding=(28, 0, 28, 12))
286
+ progress_frame = ttk.Frame(self.root, padding=(24, 0, 24, 8))
200
287
  progress_frame.grid(row=3, column=0, sticky="nsew")
201
288
  progress_frame.columnconfigure(0, weight=1)
202
289
  progress_frame.rowconfigure(2, weight=1)
203
290
  self.progress = ttk.Progressbar(progress_frame, mode="determinate", maximum=100)
204
291
  self.progress.grid(row=0, column=0, sticky="ew")
205
292
  ttk.Label(progress_frame, textvariable=self.status_var, style="Subtitle.TLabel").grid(
206
- row=1, column=0, sticky="w", pady=(6, 8)
293
+ row=1, column=0, sticky="w", pady=(5, 6)
207
294
  )
208
295
 
209
296
  log_frame = ttk.LabelFrame(progress_frame, text="Log / Output", padding=8)
@@ -212,7 +299,7 @@ class FeatureRankGUI:
212
299
  log_frame.rowconfigure(0, weight=1)
213
300
  self.log_text = tk.Text(
214
301
  log_frame,
215
- height=13,
302
+ height=8,
216
303
  wrap="word",
217
304
  state="disabled",
218
305
  background="#f8fafc",
@@ -226,19 +313,19 @@ class FeatureRankGUI:
226
313
  scrollbar.grid(row=0, column=1, sticky="ns")
227
314
  self.log_text.configure(yscrollcommand=scrollbar.set)
228
315
 
229
- summary = ttk.LabelFrame(self.root, text="Summary", padding=14)
230
- summary.grid(row=4, column=0, padx=28, pady=(0, 10), sticky="ew")
316
+ summary = ttk.LabelFrame(self.root, text="Summary", padding=12)
317
+ summary.grid(row=4, column=0, padx=24, pady=(0, 8), sticky="ew")
231
318
  summary.columnconfigure(1, weight=1)
232
319
  for row, (key, value) in enumerate(self.summary_vars.items()):
233
320
  ttk.Label(summary, text=key).grid(row=row // 2, column=(row % 2) * 2, sticky="w")
234
321
  ttk.Label(summary, textvariable=value, foreground="#1f4e79").grid(
235
- row=row // 2, column=(row % 2) * 2 + 1, sticky="w", padx=(8, 26), pady=2
322
+ row=row // 2, column=(row % 2) * 2 + 1, sticky="w", padx=(8, 22), pady=2
236
323
  )
237
324
 
238
325
  self.open_button = ttk.Button(
239
326
  self.root, text="Open Results Folder", command=self._open_results, state="disabled"
240
327
  )
241
- self.open_button.grid(row=5, column=0, padx=28, pady=(0, 18), sticky="w")
328
+ self.open_button.grid(row=5, column=0, padx=24, pady=(0, 14), sticky="w")
242
329
 
243
330
  def _browse_dataset(self) -> None:
244
331
  selected = filedialog.askopenfilename(
@@ -246,14 +333,20 @@ class FeatureRankGUI:
246
333
  filetypes=[("CSV files", "*.csv"), ("Text files", "*.txt"), ("All files", "*.*")],
247
334
  )
248
335
  if selected:
249
- self.dataset_var.set(selected)
336
+ # Keep the full path internally, but show only the file name in
337
+ # the form so the selected dataset remains readable.
338
+ self.dataset_paths[Path(selected).name] = selected
339
+ self.dataset_labels = [NO_DATASET_LABEL, *self.dataset_paths]
340
+ self.dataset_combo.configure(values=self.dataset_labels)
341
+ self.dataset_var.set(Path(selected).name)
250
342
 
251
343
  def _mode_changed(self, _event: object = None) -> None:
252
344
  is_dc = self.mode_var.get().startswith("DC")
253
345
  self.block_combo.configure(state="readonly" if is_dc else "disabled")
254
346
 
255
347
  def _start(self) -> None:
256
- dataset = self.dataset_var.get().strip()
348
+ dataset_label = self.dataset_var.get().strip()
349
+ dataset = self.dataset_paths.get(dataset_label, "")
257
350
  if not dataset:
258
351
  messagebox.showerror("FeatureRank", "Please select a dataset.")
259
352
  return
@@ -273,10 +366,13 @@ class FeatureRankGUI:
273
366
  self.last_output_dir = None
274
367
  self.open_button.configure(state="disabled")
275
368
  self.start_button.configure(state="disabled")
276
- self.progress.configure(value=2)
369
+ self.progress_value = 2.0
370
+ self.progress.configure(value=self.progress_value)
277
371
  self.stage_var.set("Loading Dataset")
278
372
  self.status_var.set("The experiment is running. You can follow progress in the log.")
279
373
  self._clear_log()
374
+ self.progress_started_at = time.perf_counter()
375
+ self.progress_running = True
280
376
 
281
377
  self.worker = threading.Thread(
282
378
  target=self._run_experiment,
@@ -284,12 +380,14 @@ class FeatureRankGUI:
284
380
  daemon=True,
285
381
  )
286
382
  self.worker.start()
383
+ self._animate_progress()
287
384
  self.root.after(100, self._poll_messages)
288
385
 
289
386
  def _run_experiment(
290
387
  self, dataset: str, percent: float, task: str, mode: str, block_count: int
291
388
  ) -> None:
292
389
  started_at = time.perf_counter()
390
+ output_lines: list[str] = []
293
391
  try:
294
392
  # Keep TensorFlow out of the Tk process. On macOS, loading a model
295
393
  # from a GUI thread can terminate the native Python process. The
@@ -297,8 +395,9 @@ class FeatureRankGUI:
297
395
  # stdout is streamed back into this window.
298
396
  from src.Config import ExperimentConfig
299
397
 
398
+ dataset_path = _resolve_dataset_path(dataset)
300
399
  config = ExperimentConfig(
301
- dataset_name=dataset,
400
+ dataset_name=dataset_path,
302
401
  task=task,
303
402
  feature_percent=percent,
304
403
  random_state=42,
@@ -308,15 +407,13 @@ class FeatureRankGUI:
308
407
  cluster_k=None,
309
408
  save_details=False,
310
409
  )
311
- python_executable = Path(sys.executable).with_name("python")
312
- if not python_executable.exists():
313
- python_executable = Path(sys.executable)
410
+ python_executable = _worker_python()
314
411
  command = [
315
412
  str(python_executable),
316
413
  "-m",
317
414
  "scripts.FeatureRank",
318
415
  "--dataset-name",
319
- dataset,
416
+ dataset_path,
320
417
  "--task",
321
418
  task,
322
419
  "--feature-percent",
@@ -328,9 +425,18 @@ class FeatureRankGUI:
328
425
  if mode == "dc":
329
426
  command.extend(["--block-count", str(block_count)])
330
427
 
428
+ worker_environment = os.environ.copy()
429
+ worker_environment["PYTHONUNBUFFERED"] = "1"
430
+ package_root = str(Path(__file__).resolve().parent.parent)
431
+ existing_python_path = worker_environment.get("PYTHONPATH")
432
+ worker_environment["PYTHONPATH"] = os.pathsep.join(
433
+ path for path in (package_root, existing_python_path) if path
434
+ )
435
+ self.messages.put(("log", f"Worker: {python_executable}"))
331
436
  process = subprocess.Popen(
332
437
  command,
333
438
  cwd=Path.cwd(),
439
+ env=worker_environment,
334
440
  stdout=subprocess.PIPE,
335
441
  stderr=subprocess.STDOUT,
336
442
  text=True,
@@ -338,6 +444,7 @@ class FeatureRankGUI:
338
444
  )
339
445
  assert process.stdout is not None
340
446
  for line in process.stdout:
447
+ output_lines.append(line.rstrip())
341
448
  self.messages.put(("log", line))
342
449
  return_code = process.wait()
343
450
  if return_code != 0:
@@ -347,7 +454,9 @@ class FeatureRankGUI:
347
454
  "Anaconda TensorFlow paketini guncelleyin veya uyumlu bir "
348
455
  "Python yorumlayicisi kullanin."
349
456
  )
350
- raise RuntimeError(f"FeatureRank worker stopped with code {return_code}.")
457
+ recent_output = "\n".join(line for line in output_lines[-12:] if line.strip())
458
+ details = f"\n\nSon worker çıktısı:\n{recent_output}" if recent_output else ""
459
+ raise RuntimeError(f"FeatureRank worker stopped with code {return_code}.{details}")
351
460
 
352
461
  average = 0.0
353
462
  values: list[float] = []
@@ -489,11 +598,31 @@ class FeatureRankGUI:
489
598
  elif "metrik" in lower or "metric" in lower or "tamamlandi" in lower:
490
599
  self._set_progress("Calculating Results", 95)
491
600
 
601
+ def _animate_progress(self) -> None:
602
+ """Advance the bar smoothly while the worker is still running.
603
+
604
+ Workflow messages move the bar to meaningful stages. This timer fills
605
+ the gaps between those messages based on elapsed time, then waits near
606
+ 90% until the worker reports completion instead of pretending to know
607
+ the exact duration of every dataset.
608
+ """
609
+ if not self.progress_running or self.progress_started_at is None:
610
+ return
611
+ elapsed = time.perf_counter() - self.progress_started_at
612
+ time_based_value = min(90.0, 2.0 + elapsed * 1.2)
613
+ if time_based_value > self.progress_value:
614
+ self.progress_value = time_based_value
615
+ self.progress.configure(value=self.progress_value)
616
+ self.status_var.set(f"The experiment is running. Elapsed time: {elapsed:.0f} s.")
617
+ self.progress_job = self.root.after(200, self._animate_progress)
618
+
492
619
  def _set_progress(self, stage: str, value: int) -> None:
493
620
  self.stage_var.set(stage)
494
- self.progress.configure(value=min(value, 99))
621
+ self.progress_value = max(self.progress_value, float(min(value, 99)))
622
+ self.progress.configure(value=self.progress_value)
495
623
 
496
624
  def _completed(self, summary: dict[str, str]) -> None:
625
+ self._stop_progress_animation()
497
626
  for key, value in summary.items():
498
627
  self.summary_vars[key].set(value)
499
628
  output = summary.get("Output Directory", "N/A")
@@ -506,13 +635,22 @@ class FeatureRankGUI:
506
635
  self.start_button.configure(state="normal")
507
636
 
508
637
  def _failed(self, error: str) -> None:
638
+ self._stop_progress_animation()
509
639
  self.progress.configure(value=0)
640
+ self.progress_value = 0.0
510
641
  self.stage_var.set("Failed")
511
642
  self.status_var.set("The experiment could not be completed.")
512
643
  self._append_log(f"ERROR: {error}")
513
644
  self.start_button.configure(state="normal")
514
645
  messagebox.showerror("FeatureRank", error)
515
646
 
647
+ def _stop_progress_animation(self) -> None:
648
+ self.progress_running = False
649
+ self.progress_started_at = None
650
+ if self.progress_job is not None:
651
+ self.root.after_cancel(self.progress_job)
652
+ self.progress_job = None
653
+
516
654
  def _clear_log(self) -> None:
517
655
  self.log_text.configure(state="normal")
518
656
  self.log_text.delete("1.0", tk.END)
@@ -11,7 +11,7 @@ import sys
11
11
 
12
12
  from .GUI import Launch
13
13
 
14
- __version__ = "0.1.2"
14
+ __version__ = "0.1.4"
15
15
 
16
16
 
17
17
  def _run_cli(arguments: list[str] | None = None) -> None:
@@ -1,17 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: FeatureRank
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Autoencoder-based feature ranking and selection workflows
5
5
  Author: Sercan
6
6
  License: MIT
7
- Requires-Python: >=3.13
7
+ Requires-Python: <3.14,>=3.10
8
8
  Description-Content-Type: text/markdown
9
9
  Requires-Dist: numpy>=2.0
10
10
  Requires-Dist: pandas>=2.0
11
11
  Requires-Dist: numexpr>=2.10.2
12
12
  Requires-Dist: scikit-learn>=1.4
13
13
  Requires-Dist: matplotlib>=3.8
14
- Requires-Dist: tensorflow>=2.21
14
+ Requires-Dist: tensorflow<2.22,>=2.21; platform_system != "Darwin" or platform_machine != "x86_64"
15
+ Requires-Dist: tensorflow<2.17,>=2.16.2; platform_system == "Darwin" and platform_machine == "x86_64" and python_version < "3.13"
15
16
 
16
17
  # FeatureRank
17
18
 
@@ -28,23 +29,45 @@ The project supports two selection modes:
28
29
  The selected features can be evaluated with classification, regression, or
29
30
  unsupervised clustering.
30
31
 
31
- ## Installation
32
+ ## PyPI installation and GUI (end users)
32
33
 
33
- Use Python 3.13. For the published package, install the latest release with:
34
+ FeatureRank supports 64-bit Python 3.10, 3.11, 3.12, and 3.13 on desktop
35
+ Windows, Linux, and macOS systems. Python 3.14 is not supported yet. Intel
36
+ macOS users should use Python 3.12 or earlier because newer TensorFlow releases
37
+ do not provide an Intel macOS wheel. Check the Python version first:
34
38
 
35
39
  ```bash
36
- python -m pip install --upgrade FeatureRank
40
+ python3 --version
37
41
  ```
38
42
 
39
- The package name is case-insensitive for `pip`, while the Python import name
40
- is `FeatureRank`. Verify the installation from Python (not directly in zsh):
43
+ Install the published package with:
41
44
 
42
45
  ```bash
43
- python
46
+ pip install FeatureRank
47
+ ```
48
+
49
+ ![Installing FeatureRank with pip](docs/images/01-pip-install.png)
50
+
51
+ Open Python:
52
+
53
+ ```bash
54
+ python3
55
+ ```
56
+
57
+ ![Opening Python 3](docs/images/02-python3.png)
58
+
59
+ Then import FeatureRank at the Python prompt:
60
+
61
+ ```python
44
62
  >>> import FeatureRank
45
- >>> print(FeatureRank.__file__)
46
63
  ```
47
64
 
65
+ ![Importing FeatureRank](docs/images/03-import-featurerank.png)
66
+
67
+ The import opens the desktop GUI automatically. Do not type `import
68
+ FeatureRank` directly in zsh; it is Python code and must be entered after
69
+ starting `python3`.
70
+
48
71
  For local development, run these commands from the repository root instead:
49
72
 
50
73
  ```bash
@@ -58,10 +81,10 @@ GPU-specific dependencies are listed in
58
81
  [`requirements-gpu.txt`](requirements-gpu.txt). Hardware notes are available in
59
82
  [`GPU_SETUP.md`](GPU_SETUP.md).
60
83
 
61
- The repository metadata is version `0.1.2`. Because PyPI releases cannot be
84
+ The repository metadata is version `0.1.4`. Because PyPI releases cannot be
62
85
  replaced in place, publish this version (or a newer one) to the existing
63
86
  `FeatureRank` project when you release the refactor. If an environment still
64
- shows `FeatureRank==0.1.0`, upgrade it with the command above or use the local
87
+ shows an older FeatureRank version, upgrade it with the command above or use the local
65
88
  editable installation while developing.
66
89
 
67
90
  After installation, the package GUI can be opened with:
@@ -96,12 +119,17 @@ in that interpreter's environment.
96
119
  If you do not want to install the package, use the equivalent script directly:
97
120
 
98
121
  ```bash
99
- python scripts/FeatureRank.py --help
122
+ python3 scripts/FeatureRank.py --help
100
123
  ```
101
124
 
102
- ## Using FeatureRank
125
+ ## Optional Python API (developers)
126
+
127
+ The main entry point for regular users is the GUI shown above. The following
128
+ Python API is optional and is useful when another Python program needs to start
129
+ an experiment. It is not required for the normal `pip install` → `import
130
+ FeatureRank` workflow.
103
131
 
104
- The main entry point is:
132
+ The main command-line entry point is:
105
133
 
106
134
  ```text
107
135
  scripts/FeatureRank.py
@@ -136,28 +164,45 @@ The GUI is the primary end-user interface. Select a dataset from the dropdown
136
164
  or choose a CSV/TXT file with **Browse…**, select a feature percentage, choose
137
165
  `GL` or `DC`, and press **START**. `Block Count` is enabled only for `DC`.
138
166
 
167
+ When the GUI opens, the experiment form is ready for these choices:
168
+
169
+ ![FeatureRank GUI](docs/images/04-gui-ready.png)
170
+
171
+ If the required dataset is not listed, press **Browse…** and select its data
172
+ file. For paired datasets, keep the matching label file in the same directory:
173
+
174
+ ![Selecting a dataset](docs/images/05-browse-dataset.png)
175
+
139
176
  While the model runs, the progress bar and log show stages such as loading,
140
177
  feature ranking, block processing, combining, training, and result creation.
141
178
  When the run finishes, the summary lists the selected feature count, metric,
142
179
  execution time, and output directory. **Open Results Folder** opens that
143
180
  directory in Finder, Explorer, or the Linux file manager.
144
181
 
182
+ ![Completed FeatureRank result](docs/images/06-results.png)
183
+
145
184
  The selected file can be a normal CSV containing a `target` column, or one of
146
185
  the project's paired files (`*_data.csv` and `*_label.csv`).
147
186
 
148
- ## Using from Terminal
187
+ ## Local repository command-line mode (developers)
188
+
189
+ The GUI is the normal user interface. The commands in this section are optional
190
+ and are intended for developers, batch experiments, and reproducible research.
191
+ They bypass the GUI and run the existing workflow directly in the terminal.
192
+ They are available from a repository checkout; a normal PyPI user does not need
193
+ them.
149
194
 
150
- The original parameterized CLI remains available for development and backward
151
- compatibility. To inspect its options:
195
+ Run these shell commands directly in Terminal. Do not first open the Python
196
+ prompt and do not type the `>>>` marker. To inspect the CLI options:
152
197
 
153
198
  ```bash
154
- python scripts/FeatureRank.py --help
199
+ python3 scripts/FeatureRank.py --help
155
200
  ```
156
201
 
157
202
  A normal classification run is:
158
203
 
159
204
  ```bash
160
- python scripts/FeatureRank.py \
205
+ python3 scripts/FeatureRank.py \
161
206
  --dataset-name breast_cancer_data.csv \
162
207
  --task classification \
163
208
  --feature-percent 20 \
@@ -167,7 +212,7 @@ python scripts/FeatureRank.py \
167
212
  The command can also be run through the compatibility wrapper:
168
213
 
169
214
  ```bash
170
- python scripts/RunAutoencoder.py \
215
+ python3 scripts/RunAutoencoder.py \
171
216
  --dataset-name breast_cancer_data.csv \
172
217
  --feature-percent 20
173
218
  ```
@@ -189,7 +234,7 @@ workflow is:
189
234
  Run GLOBAL explicitly with:
190
235
 
191
236
  ```bash
192
- python scripts/FeatureRank.py \
237
+ python3 scripts/FeatureRank.py \
193
238
  --dataset-name carcinom_data.csv \
194
239
  --task classification \
195
240
  --feature-percent 40 \
@@ -218,7 +263,7 @@ The steps are:
218
263
  Run DC with ten blocks:
219
264
 
220
265
  ```bash
221
- python scripts/FeatureRank.py \
266
+ python3 scripts/FeatureRank.py \
222
267
  --dataset-name arcene_data.csv \
223
268
  --task classification \
224
269
  --feature-percent 50 \
@@ -284,19 +329,19 @@ Examples:
284
329
 
285
330
  ```bash
286
331
  # Classification
287
- python scripts/FeatureRank.py \
332
+ python3 scripts/FeatureRank.py \
288
333
  --dataset-name breast_cancer_data.csv \
289
334
  --task classification \
290
335
  --feature-percent 20
291
336
 
292
337
  # Regression
293
- python scripts/FeatureRank.py \
338
+ python3 scripts/FeatureRank.py \
294
339
  --dataset-name air_data.csv \
295
340
  --task regression \
296
341
  --feature-percent 30
297
342
 
298
343
  # Clustering
299
- python scripts/FeatureRank.py \
344
+ python3 scripts/FeatureRank.py \
300
345
  --dataset-name codon_usage_data.csv \
301
346
  --task clustering \
302
347
  --feature-percent 60
@@ -322,7 +367,7 @@ The main user-facing parameters are:
322
367
  For the complete, current list:
323
368
 
324
369
  ```bash
325
- python scripts/FeatureRank.py --help
370
+ python3 scripts/FeatureRank.py --help
326
371
  ```
327
372
 
328
373
  Model architecture, epochs, batch size, learning rate, early stopping, and
@@ -423,9 +468,9 @@ in [`requirements-gpu.txt`](requirements-gpu.txt).
423
468
  Run the checks from the repository root:
424
469
 
425
470
  ```bash
426
- python -m pytest -q
427
- python -m pyflakes src scripts tests
428
- python -m compileall -q src scripts tests
471
+ python3 -m pytest -q
472
+ python3 -m pyflakes src scripts tests
473
+ python3 -m compileall -q src scripts tests
429
474
  ```
430
475
 
431
476
  ## Citation