MIDRC-MELODY 0.3.3__py3-none-any.whl

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 (37) hide show
  1. MIDRC_MELODY/__init__.py +0 -0
  2. MIDRC_MELODY/__main__.py +4 -0
  3. MIDRC_MELODY/common/__init__.py +0 -0
  4. MIDRC_MELODY/common/data_loading.py +199 -0
  5. MIDRC_MELODY/common/data_preprocessing.py +134 -0
  6. MIDRC_MELODY/common/edit_config.py +156 -0
  7. MIDRC_MELODY/common/eod_aaod_metrics.py +292 -0
  8. MIDRC_MELODY/common/generate_eod_aaod_spiders.py +69 -0
  9. MIDRC_MELODY/common/generate_qwk_spiders.py +56 -0
  10. MIDRC_MELODY/common/matplotlib_spider.py +425 -0
  11. MIDRC_MELODY/common/plot_tools.py +132 -0
  12. MIDRC_MELODY/common/plotly_spider.py +217 -0
  13. MIDRC_MELODY/common/qwk_metrics.py +244 -0
  14. MIDRC_MELODY/common/table_tools.py +230 -0
  15. MIDRC_MELODY/gui/__init__.py +0 -0
  16. MIDRC_MELODY/gui/config_editor.py +200 -0
  17. MIDRC_MELODY/gui/data_loading.py +157 -0
  18. MIDRC_MELODY/gui/main_controller.py +154 -0
  19. MIDRC_MELODY/gui/main_window.py +545 -0
  20. MIDRC_MELODY/gui/matplotlib_spider_widget.py +204 -0
  21. MIDRC_MELODY/gui/metrics_model.py +62 -0
  22. MIDRC_MELODY/gui/plotly_spider_widget.py +56 -0
  23. MIDRC_MELODY/gui/qchart_spider_widget.py +272 -0
  24. MIDRC_MELODY/gui/shared/__init__.py +0 -0
  25. MIDRC_MELODY/gui/shared/react/__init__.py +0 -0
  26. MIDRC_MELODY/gui/shared/react/copyabletableview.py +100 -0
  27. MIDRC_MELODY/gui/shared/react/grabbablewidget.py +406 -0
  28. MIDRC_MELODY/gui/tqdm_handler.py +210 -0
  29. MIDRC_MELODY/melody.py +102 -0
  30. MIDRC_MELODY/melody_gui.py +111 -0
  31. MIDRC_MELODY/resources/MIDRC.ico +0 -0
  32. midrc_melody-0.3.3.dist-info/METADATA +151 -0
  33. midrc_melody-0.3.3.dist-info/RECORD +37 -0
  34. midrc_melody-0.3.3.dist-info/WHEEL +5 -0
  35. midrc_melody-0.3.3.dist-info/entry_points.txt +4 -0
  36. midrc_melody-0.3.3.dist-info/licenses/LICENSE +201 -0
  37. midrc_melody-0.3.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,210 @@
1
+ """ANSI escape sequence processor and worker/thread helpers for GUI."""
2
+
3
+ import re
4
+ from typing import Final, Optional
5
+
6
+ from PySide6.QtCore import QObject, QRunnable, Signal
7
+ from PySide6.QtGui import QFont, QTextCharFormat, QTextCursor
8
+ from PySide6.QtWidgets import QPlainTextEdit
9
+
10
+ __all__ = ["ANSIProcessor", "EmittingStream", "Worker"]
11
+
12
+ _CSI_RE: Final[re.Pattern] = re.compile(r"\x1b\[(\d*)([A-Za-z])")
13
+
14
+
15
+ def _next_csi_is_cursor_up(buf: str, pos: int) -> bool:
16
+ """Return True if next CSI sequence at pos is 'cursor up' (A)."""
17
+ if pos >= len(buf) or buf[pos] != "\x1b":
18
+ return False
19
+ match = _CSI_RE.match(buf, pos)
20
+ return bool(match and match.group(2) == "A")
21
+
22
+
23
+ class ANSIProcessor:
24
+ """Process ANSI control sequences and render them in a QPlainTextEdit."""
25
+
26
+ @staticmethod
27
+ def process(console: QPlainTextEdit, chunk: str) -> None:
28
+ """
29
+ Parse chunk for ANSI escapes, updating the console widget.
30
+
31
+ Args:
32
+ console: Target QPlainTextEdit.
33
+ chunk: Text with possible ANSI sequences.
34
+ """
35
+ ANSIProcessor._ensure_console_flags(console)
36
+ cursor = console.textCursor()
37
+ i, n = 0, len(chunk)
38
+ text_buf: list[str] = []
39
+
40
+ while i < n:
41
+ if console._nl_pending: # type: ignore[attr-defined]
42
+ ANSIProcessor._commit_pending_newline(console, cursor, chunk, i)
43
+
44
+ ch = chunk[i]
45
+ if ch == "\r": # Handle carriage return
46
+ if text_buf:
47
+ cursor.insertText("".join(text_buf))
48
+ text_buf.clear()
49
+ ANSIProcessor._handle_carriage_return(cursor)
50
+ i += 1
51
+ continue
52
+
53
+ if ch == "\n": # Handle line feed
54
+ if text_buf:
55
+ cursor.insertText("".join(text_buf))
56
+ text_buf.clear()
57
+ ANSIProcessor._handle_line_feed(cursor)
58
+ i += 1
59
+ continue
60
+
61
+ if ch == "\x1b": # Handle CSI (Control Sequence Introducer) sequence
62
+ if text_buf:
63
+ cursor.insertText("".join(text_buf))
64
+ text_buf.clear()
65
+ new_pos = ANSIProcessor._handle_csi(console, cursor, chunk, i)
66
+ if new_pos is not None:
67
+ i = new_pos
68
+ continue
69
+
70
+ text_buf.append(ch)
71
+ i += 1
72
+
73
+ if text_buf:
74
+ cursor.insertText("".join(text_buf))
75
+
76
+ console.setTextCursor(cursor)
77
+ console.ensureCursorVisible()
78
+
79
+ @staticmethod
80
+ def _ensure_console_flags(console: QPlainTextEdit) -> None:
81
+ """Initialize internal flags for ANSI processing on the widget."""
82
+ if not hasattr(console, "_nl_pending"):
83
+ console._nl_pending = False # type: ignore[attr-defined]
84
+ if not hasattr(console, "_ansi_fmt"):
85
+ console._ansi_fmt = QTextCharFormat() # type: ignore[attr-defined]
86
+
87
+ @staticmethod
88
+ def _commit_pending_newline(
89
+ console: QPlainTextEdit, cursor: QTextCursor, buf: str, pos: int
90
+ ) -> None:
91
+ """Insert a newline unless the next sequence is cursor-up."""
92
+ if not _next_csi_is_cursor_up(buf, pos):
93
+ cursor.insertBlock()
94
+ console._nl_pending = False # type: ignore[attr-defined]
95
+
96
+ @staticmethod
97
+ def _handle_carriage_return(cursor: QTextCursor) -> None:
98
+ """Handle '\\r': move to start of line and clear it."""
99
+ cursor.movePosition(QTextCursor.StartOfLine)
100
+ cursor.select(QTextCursor.LineUnderCursor)
101
+ cursor.removeSelectedText()
102
+
103
+ @staticmethod
104
+ def _handle_line_feed(cursor: QTextCursor) -> None:
105
+ """Handle '\\n': move down or insert a new block then start of line."""
106
+ if not cursor.movePosition(QTextCursor.Down):
107
+ cursor.insertBlock()
108
+ cursor.movePosition(QTextCursor.StartOfLine)
109
+
110
+ @staticmethod
111
+ def _handle_csi(
112
+ console: QPlainTextEdit,
113
+ cursor: QTextCursor,
114
+ buf: str,
115
+ pos: int,
116
+ ) -> Optional[int]:
117
+ """
118
+ Process CSI (Control Sequence Introducer) sequences.
119
+
120
+ Returns:
121
+ Position after CSI if handled, otherwise None.
122
+ """
123
+ match = _CSI_RE.match(buf, pos)
124
+ if not match:
125
+ return None
126
+
127
+ count = int(match.group(1) or 1)
128
+ cmd = match.group(2)
129
+
130
+ if cmd == "A": # Cursor Up
131
+ for _ in range(count):
132
+ cursor.movePosition(QTextCursor.Up)
133
+ cursor.movePosition(QTextCursor.StartOfLine)
134
+ return match.end()
135
+
136
+ if cmd == "K": # Erase Line
137
+ cursor.select(QTextCursor.LineUnderCursor)
138
+ cursor.removeSelectedText()
139
+ cursor.movePosition(QTextCursor.StartOfLine)
140
+ return match.end()
141
+
142
+ if cmd == "m": # Set Graphics Rendition (text style: e.g. bold, reset, etc.)
143
+ for part in (match.group(1) or "0").split(";"):
144
+ style = part or "0"
145
+ if style == "0":
146
+ console._ansi_fmt = QTextCharFormat() # type: ignore[attr-defined]
147
+ elif style == "1":
148
+ console._ansi_fmt.setFontWeight(QFont.Bold)
149
+ elif style == "22":
150
+ console._ansi_fmt.setFontWeight(QFont.Normal)
151
+ cursor.setCharFormat(console._ansi_fmt)
152
+ return match.end()
153
+
154
+ return None
155
+
156
+
157
+ class EmittingStream(QObject):
158
+ """A text stream that emits data via Qt signals."""
159
+
160
+ textWritten: Signal = Signal(str)
161
+
162
+ def write(self, data: str) -> None:
163
+ """Emit chunk of text written to this stream."""
164
+ text = str(data)
165
+ if text:
166
+ self.textWritten.emit(text)
167
+
168
+ def flush(self) -> None:
169
+ """No-op flush to satisfy stream interface."""
170
+ pass
171
+
172
+ def isatty(self) -> bool:
173
+ """Indicate this stream behaves like a terminal."""
174
+ return True
175
+
176
+
177
+ class WorkerSignals(QObject):
178
+ """Signals for Worker: finished, error, and result."""
179
+ finished: Signal = Signal()
180
+ error: Signal = Signal(str)
181
+ result: Signal = Signal(object)
182
+
183
+
184
+ class Worker(QRunnable):
185
+ """
186
+ QRunnable wrapper to execute a function in a separate thread.
187
+
188
+ Args:
189
+ fn: Callable to run.
190
+ *args: Positional args for fn.
191
+ **kwargs: Keyword args for fn.
192
+ """
193
+
194
+ def __init__(self, fn, *args, **kwargs) -> None:
195
+ super().__init__()
196
+ self.fn = fn
197
+ self.args = args
198
+ self.kwargs = kwargs
199
+ self.signals = WorkerSignals()
200
+
201
+ def run(self) -> None:
202
+ """Execute the function and emit result, error, and finished signals."""
203
+ try:
204
+ result = self.fn(*self.args, **self.kwargs)
205
+ except Exception as exc:
206
+ self.signals.error.emit(str(exc))
207
+ else:
208
+ self.signals.result.emit(result)
209
+ finally:
210
+ self.signals.finished.emit()
MIDRC_MELODY/melody.py ADDED
@@ -0,0 +1,102 @@
1
+ # Copyright (c) 2025 Medical Imaging and Data Resource Center (MIDRC).
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+
16
+ import sys
17
+
18
+ from MIDRC_MELODY.common.edit_config import edit_config
19
+ from MIDRC_MELODY.common.generate_eod_aaod_spiders import generate_eod_aaod_spiders
20
+ from MIDRC_MELODY.common.generate_qwk_spiders import generate_qwk_spiders
21
+
22
+ try:
23
+ from PySide6.QtWidgets import QWidget # noqa: F401
24
+ GUI_AVAILABLE = True
25
+ except ImportError:
26
+ GUI_AVAILABLE = False
27
+
28
+
29
+ def show_config(path):
30
+ """Print out the YAML config file at `path`."""
31
+ try:
32
+ print(open(path, encoding='utf-8').read())
33
+ except Exception as e:
34
+ print(f"Error reading config: {e}")
35
+
36
+
37
+ def _launch_gui():
38
+ """Launch the GUI if available."""
39
+ try:
40
+ from MIDRC_MELODY.melody_gui import launch_gui
41
+ except ImportError as e1:
42
+ try:
43
+ from melody_gui import launch_gui
44
+ except ImportError as e2:
45
+ e = e2 if str(e1) == "No module named 'MIDRC_MELODY.melody_gui'" else (
46
+ str(e1) + "\n" + str(e2) if str(e2) != "No module named 'melody_gui'" else e1)
47
+ print('-' * 50)
48
+ print(f"GUI is not available, import failed with error:\n{e}")
49
+ print('-' * 50)
50
+ return
51
+ launch_gui()
52
+
53
+
54
+ def set_config(current_path):
55
+ """Prompt for a new path and return it (or the old one)."""
56
+ new = input(f"Enter new config path [{current_path}]: ").strip()
57
+ return new or current_path
58
+
59
+
60
+ def quit_program(_=None):
61
+ print("Goodbye!")
62
+ sys.exit(0)
63
+
64
+
65
+ def main():
66
+ # wrap your mutable state in a dict
67
+ state = {"config_path": "config.yaml"}
68
+
69
+ commands = {
70
+ "1": ("Calculate QWK metrics",
71
+ lambda: generate_qwk_spiders(cfg_path=state["config_path"])),
72
+ "2": ("Calculate EOD/AAOD metrics",
73
+ lambda: generate_eod_aaod_spiders(cfg_path=state["config_path"])),
74
+ }
75
+ if GUI_AVAILABLE:
76
+ commands["3"] = ("Launch GUI", lambda: _launch_gui())
77
+ commands["s"] = ("Show current config file contents", lambda: show_config(state["config_path"]))
78
+ commands["f"] = ("Change config file path", lambda: state.update(config_path=set_config(state["config_path"])))
79
+ commands["e"] = ("Edit config file", lambda: edit_config(state["config_path"]))
80
+ commands["q"] = ("Quit", quit_program)
81
+
82
+ BOLD = '\033[1m'
83
+ RESET = '\033[0m'
84
+ while True:
85
+ print("\n=== MIDRC‑MELODY Menu ===")
86
+ print(f"Current config file path: {BOLD}{state['config_path']}{RESET}")
87
+ for key, (desc, _) in commands.items():
88
+ print(f" {key}) {desc}")
89
+ choice = input("Select an option: ").strip()
90
+
91
+ action = commands.get(choice)
92
+ if not action:
93
+ print(f" ❌ '{choice}' is not a valid choice.")
94
+ continue
95
+
96
+ # call the handler
97
+ _, handler = action
98
+ handler()
99
+
100
+
101
+ if __name__ == "__main__":
102
+ main()
@@ -0,0 +1,111 @@
1
+ # Copyright (c) 2025 Medical Imaging and Data Resource Center (MIDRC).
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+
16
+ from importlib import resources as _resources
17
+ import sys
18
+ try:
19
+ from PySide6.QtWidgets import QApplication
20
+ from PySide6.QtCore import QTimer, Qt
21
+ from PySide6.QtGui import QIcon, QPixmap
22
+ from MIDRC_MELODY.gui.main_window import MainWindow # Import the custom MainWindow
23
+ except ImportError as e:
24
+ raise ImportError("To use the GUI features, please install the package PySide6.\n"
25
+ "You can install it using the command:\n"
26
+ "pip install PySide6\n"
27
+ "\n"
28
+ "Error details: " + str(e))
29
+
30
+
31
+ # Global list to hold window references.
32
+ _open_windows = []
33
+
34
+ def _load_package_icon() -> QIcon:
35
+ """
36
+ Load the bundled icon from the installed package resources.
37
+ Uses importlib.resources.files(...) with the current package name.
38
+ """
39
+ try:
40
+ pkg = __package__ or "MIDRC_MELODY"
41
+ data = _resources.files(pkg).joinpath("resources", "MIDRC.ico").read_bytes()
42
+ pix = QPixmap()
43
+ pix.loadFromData(data)
44
+ return QIcon(pix)
45
+ except Exception:
46
+ return QIcon()
47
+
48
+ # optional: import ctypes only on Windows
49
+ _ctypes = None
50
+ if sys.platform == "win32":
51
+ try:
52
+ import ctypes as _ctypes
53
+ except Exception:
54
+ _ctypes = None
55
+
56
+ def _set_windows_appid(appid: str) -> None:
57
+ """
58
+ On Windows set an explicit AppUserModelID so the taskbar uses the correct icon.
59
+ Must be called before creating any windows / QApplication.
60
+ """
61
+ if sys.platform != "win32" or _ctypes is None:
62
+ return
63
+ try:
64
+ _ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid)
65
+ except Exception:
66
+ # best-effort; ignore failures
67
+ pass
68
+
69
+ def launch_gui() -> None:
70
+ _set_windows_appid("com.midrc.melody")
71
+ app = QApplication.instance()
72
+ if app is None:
73
+ app = QApplication([])
74
+ new_app = True
75
+ else:
76
+ new_app = False
77
+
78
+ icon = _load_package_icon()
79
+ if not icon.isNull():
80
+ app.setWindowIcon(icon)
81
+
82
+ window = MainWindow()
83
+ if not icon.isNull():
84
+ window.setWindowIcon(icon)
85
+ _open_windows.clear()
86
+ _open_windows.append(window)
87
+
88
+ # Store original window flags
89
+ original_flags = window.windowFlags()
90
+
91
+ window.show()
92
+ # Temporarily add the always-on-top flag without losing other flags.
93
+ QTimer.singleShot(100, lambda: (
94
+ window.setWindowFlags(original_flags | Qt.WindowStaysOnTopHint),
95
+ window.show()
96
+ ))
97
+ # After a short delay, restore the original flags with the window still in the front.
98
+ QTimer.singleShot(300, lambda: (
99
+ window.setWindowFlags(original_flags),
100
+ window.show()
101
+ ))
102
+ if new_app:
103
+ app.exec()
104
+
105
+
106
+ def main():
107
+ launch_gui()
108
+
109
+
110
+ if __name__ == "__main__":
111
+ main()
Binary file
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: MIDRC_MELODY
3
+ Version: 0.3.3
4
+ Summary: MELODY: Model EvaLuation across subgroups for cOnsistent Decision accuracY
5
+ Author-email: Robert Tomek <rtomek@uchicago.edu>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/MIDRC/MIDRC_MELODY
8
+ Project-URL: Issues, https://github.com/MIDRC/MIDRC_MELODY/issues
9
+ Keywords: machine-learning,model-evaluation,fairness,subgroup-analysis
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: pandas
13
+ Requires-Dist: matplotlib
14
+ Requires-Dist: numpy
15
+ Requires-Dist: scikit-learn
16
+ Requires-Dist: PyYAML
17
+ Requires-Dist: joblib
18
+ Requires-Dist: tqdm
19
+ Requires-Dist: tabulate
20
+ Requires-Dist: tqdm-joblib
21
+ Requires-Dist: mplcursors
22
+ Requires-Dist: windows-curses; sys_platform == "win32"
23
+ Requires-Dist: PySide6>=6.8.0
24
+ Dynamic: license-file
25
+
26
+ # MIDRC MELODY (Model EvaLuation across subgroups for cOnsistent Decision accuracY)
27
+
28
+ [Overview](#overview) | [Installation](#installation) | [CLI Commands](#cli-commands) | [Configuration](#configuration) | [License](#license)
29
+
30
+ [📱 Visit MIDRC Website](https://www.midrc.org/)
31
+
32
+ **MIDRC MELODY** is a tool designed to assess the performance and subgroup-level reliability and robustness of AI models
33
+ developed for medical imaging analysis tasks, such as the estimation of disease severity. It enables consistent
34
+ evaluation of models across predefined subgroups (e.g. manufacturer, race, scanner type) by computing intergroup
35
+ performance metrics and corresponding confidence intervals.
36
+
37
+ The tool supports two types of evaluation:
38
+
39
+ - **Ordinal Estimation Task Evaluation**:
40
+ - Uses an ordinal reference standard ("truth") and ordinal AI model outputs.
41
+ - Performance in terms of agreement of AI output with the reference standard "truth" is quantified using the **quadratic
42
+ weighted kappa (QWK)** metric.
43
+ - **Binary Decision Task Evaluation**:
44
+ - Converts scores into binary decisions based on a threshold.
45
+ - Computes **Equal Opportunity Difference (EOD)** and **Average Absolute Odds Difference (AAOD)** metrics using bootstrapping across various groups.
46
+ - Generates spider plots comparing these metrics.
47
+ - Saves the generated data for further analysis.
48
+
49
+ ### Data Processing and Visualization
50
+
51
+ - **Bootstrapping:** Both scripts perform bootstrapping to compute confidence intervals for the respective metrics using NumPy's percentile method.
52
+ - **Plotting:** Spider charts provide a visual overview of how each model's metrics vary across different groups and categories.
53
+ - **Utilities:** Shared functionality is available in common utility modules (e.g., `data_tools.py` and `plot_tools.py`), ensuring easier maintenance and testing.
54
+
55
+ ## Overview
56
+
57
+ **MIDRC MELODY** is a lightweight toolkit for stress‑testing medical‑imaging AI models across clinical and demographic sub‑groups. It supports both command‑line and GUI workflows, enabling rapid quantification of performance disparities (QWK, EOD, AAOD, etc.) and intuitive radar‑chart visualisation.
58
+
59
+ - **Console‑first** – core metrics and plots run with **no GUI dependencies**.
60
+ - **Opt‑in GUI** – an optional PySide6 interface for interactive configuration and result browsing.
61
+ - **Config‑driven** – YAML files keep experiments reproducible and shareable.
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ # Standard console install from PyPI
67
+ pip install midrc-melody
68
+
69
+ # (Alternative) Install in editable/development mode from source code
70
+ pip install -e .
71
+ ```
72
+
73
+ ## Quick Start
74
+
75
+ ```bash
76
+ # Run analysis (reads default config.yaml in current directory)
77
+ melody
78
+
79
+ # Launch the GUI
80
+ melody_gui
81
+ ```
82
+
83
+ ## CLI Commands
84
+
85
+ Running `melody` opens a **Command‑Line Interface (CLI)**, which presents a text‑based menu of interactive commands. Here’s what you can do:
86
+
87
+ #### Available Commands
88
+
89
+ - **Calculate QWK metrics**: Computes delta QWK values for different subgroups and generates spider plots.
90
+ - **Calculate EOD and AAOD metrics**: Computes EOD and AAOD metrics for binary decision tasks and generates spider plots.
91
+ - **Print config file contents**: Displays the contents of the current YAML configuration file.
92
+ - **Change config file**: Prompts you to enter and set a different configuration file path.
93
+ - **Launch GUI**: Opens the Graphical User Interface (GUI) using PySide6 (requires PySide6).
94
+ - **Exit**: Exits the program.
95
+
96
+ ## GUI (Optional)
97
+
98
+ Launching the graphical interface only requires that PySide6 is installed. If you used pip, the `melody_gui` command is available.
99
+
100
+ ```bash
101
+ # Launch the GUI:
102
+ melody_gui
103
+ ```
104
+
105
+ ## Configuration
106
+
107
+ Experiments are described in a single YAML file. Below is a **minimal** example that keeps storage light and avoids plotting custom order metadata.
108
+
109
+ ```yaml
110
+ input data:
111
+ truth file: data/demo_truthNdemographics.csv
112
+ test scores: data/demo_scores.csv
113
+ uid column: case_name
114
+ truth column: truth
115
+
116
+ # Scores ≥ binary threshold are counted as positive
117
+ binary threshold: 4
118
+ min count per category: 10
119
+
120
+ bootstrap:
121
+ iterations: 1000
122
+ seed: 42 # set to null for random entropy
123
+
124
+ output:
125
+ qwk: { save: false, file prefix: output/delta_kappas_ }
126
+ eod: { save: false, file prefix: output/eod_ }
127
+ aaod: { save: false, file prefix: output/aaod_ }
128
+
129
+ numeric_cols:
130
+ age_binned:
131
+ raw column: age
132
+ bins: [0, 18, 30, 40, 50, 65, 75, 85, .inf]
133
+
134
+ plot:
135
+ clockwise: true # rotate clockwise instead of CCW
136
+ start: top # starting angle: top, bottom, left, right (t/b/l/r)
137
+ ```
138
+
139
+ ## Input Data
140
+
141
+ | File | Required Columns | Purpose | Example |
142
+ | -------------- | --------------------------- | ----------------------------------------- |----------------------------------------------------|
143
+ | **Truth file** | `uid`, `truth`, attributes… | Ground‑truth labels and subgroup columns. | [demo_truth.csv](data/demo_truthNdemographics.csv) |
144
+ | **Score file** | `uid`, `score` | Model predictions keyed to the same UID. | [demo_scores.csv](data/demo_scores.csv) |
145
+
146
+ > UID values must match between truth and score files.
147
+
148
+ ## License
149
+
150
+ Distributed under the Apache 2.0 License.
151
+
@@ -0,0 +1,37 @@
1
+ MIDRC_MELODY/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ MIDRC_MELODY/__main__.py,sha256=0iJVMq84JaIZHWWBVb2Ly_ea1diOW3V6wSAoZbABDZs,67
3
+ MIDRC_MELODY/melody.py,sha256=5K8-mROTlG2Bm2qUGYflTCf9aErW5RyfHHrbGWbq_wA,3564
4
+ MIDRC_MELODY/melody_gui.py,sha256=IIN9Lz_7ADoeaOzZGiXSifVnNyPKkv_Ak1lPPZkSTxY,3559
5
+ MIDRC_MELODY/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ MIDRC_MELODY/common/data_loading.py,sha256=MRX0B3aw9Y8x94pSWcHF-RHneoz4HGwLIQJ_BhLbcak,7648
7
+ MIDRC_MELODY/common/data_preprocessing.py,sha256=oKpfl14-nM_X9u6Rkpp6kKMGSK12zt4Cvd-rn-pxzz0,5291
8
+ MIDRC_MELODY/common/edit_config.py,sha256=q6gZiy-sA6sY9bD_tP4w1fwpFls4zHDi1_YPegoD17U,4740
9
+ MIDRC_MELODY/common/eod_aaod_metrics.py,sha256=nCJ9g1kMKA5NY-JasI3kITozsbF_XdVZcv8NOD-DNmI,11733
10
+ MIDRC_MELODY/common/generate_eod_aaod_spiders.py,sha256=qe4WkUA_j50yjzW65orWyfBuHeZG9ULcuuJ3cq3ei0A,2954
11
+ MIDRC_MELODY/common/generate_qwk_spiders.py,sha256=MRplmYU3N5AX-p8FBOVg3jI8OMo8RSwql5Sgiq-t8hI,2252
12
+ MIDRC_MELODY/common/matplotlib_spider.py,sha256=G7ILK3hS7CcxJUXtYGSXTJKyIzyh3qbLbh-jNlSJ7BM,15478
13
+ MIDRC_MELODY/common/plot_tools.py,sha256=TV3az3geV7Y3w8DszjP0umnUqLqfc6jg_rhl_6DyAmA,4874
14
+ MIDRC_MELODY/common/plotly_spider.py,sha256=asUki0EeWwonJCQhCZrC2pIquIM7HJc9oGcUeBmjuUA,9218
15
+ MIDRC_MELODY/common/qwk_metrics.py,sha256=yucVOumytQS_CvndqRpkuSs_LaRU71zTWkhZRGHT90M,10379
16
+ MIDRC_MELODY/common/table_tools.py,sha256=zMVmOQs7U4TDiyhHF1e_GYHYvmKxypSrgu6gyikLItk,9251
17
+ MIDRC_MELODY/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ MIDRC_MELODY/gui/config_editor.py,sha256=q_NNBnj4VAvCQ6-KINBe5mhUVTHKbibK7kUTvXMjkXk,9420
19
+ MIDRC_MELODY/gui/data_loading.py,sha256=AfAFfle0lMrE4BOfepzy2aSEM5k4oT279G5xFi1-Z7U,6456
20
+ MIDRC_MELODY/gui/main_controller.py,sha256=4ZJPAzzq3TE2cKy8UlkniPV6VKBf6qMVkBZNk4HZu2c,6809
21
+ MIDRC_MELODY/gui/main_window.py,sha256=1wF6qw8m_C7l3i1nGGUO9pyV9a3v4oNLVkhTYhA1Cxc,20191
22
+ MIDRC_MELODY/gui/matplotlib_spider_widget.py,sha256=yxuVPIPEtWXqmCacfsG6XexNLST1gd1ykD_BpJygMC4,8185
23
+ MIDRC_MELODY/gui/metrics_model.py,sha256=bVlQSrh8JQJUGCXgoO8Vm6Hbf-W1fKjvtcuyYTafWjQ,3046
24
+ MIDRC_MELODY/gui/plotly_spider_widget.py,sha256=SQOEATppxVfxrHTFkjD-d5F6cKvxtfBCsDbIP7RxLO8,2193
25
+ MIDRC_MELODY/gui/qchart_spider_widget.py,sha256=iBXc-w9So2re2aFFszqMV_BFcU18tV6JpO5NBo4f8Cs,10437
26
+ MIDRC_MELODY/gui/tqdm_handler.py,sha256=drMVl9c5Rc6rPogD0pMaeFvyhHTXcqtUUU2I-W6D318,7143
27
+ MIDRC_MELODY/gui/shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
+ MIDRC_MELODY/gui/shared/react/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
+ MIDRC_MELODY/gui/shared/react/copyabletableview.py,sha256=jn_9RvlX2uU8Ib6KRCqmHGbnmtvU3Hu8DrUWuimgyGU,3668
30
+ MIDRC_MELODY/gui/shared/react/grabbablewidget.py,sha256=a7Z736TmtShki8nKSWVzTlaw53-TqxIRN1r3OyOr1K4,15569
31
+ MIDRC_MELODY/resources/MIDRC.ico,sha256=xvcK2nPPuOdqNsxBNdRxOKuXbCQ6J-cTuxDKQ6yX-qI,4022
32
+ midrc_melody-0.3.3.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
33
+ midrc_melody-0.3.3.dist-info/METADATA,sha256=WEGNWJokukCKF8weu88AYv5hfvYf87F0yOoe-geU8_o,6289
34
+ midrc_melody-0.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
+ midrc_melody-0.3.3.dist-info/entry_points.txt,sha256=O9uD1ZfwtlU7C9lAMiWEbh-19GGgCOw83cMqO-9iF5E,136
36
+ midrc_melody-0.3.3.dist-info/top_level.txt,sha256=2lK9hz0vJgDTCknUwOlanlf38tRV5RrDQVFwHxwjqQ0,13
37
+ midrc_melody-0.3.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ melody = MIDRC_MELODY.melody:main
3
+ melody-gui = MIDRC_MELODY.melody_gui:main
4
+ melody_gui = MIDRC_MELODY.melody_gui:main