featrix-modelcard 1.10__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.
@@ -0,0 +1,4 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include featrix_modelcard *.py
4
+
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: featrix-modelcard
3
+ Version: 1.10
4
+ Summary: Render Featrix Sphere Model Card JSON to HTML or plain text
5
+ Home-page: https://github.com/featrix/model-card
6
+ Author: Featrix
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.7
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Requires-Python: >=3.7
18
+ Description-Content-Type: text/markdown
19
+ Dynamic: author
20
+ Dynamic: classifier
21
+ Dynamic: description
22
+ Dynamic: description-content-type
23
+ Dynamic: home-page
24
+ Dynamic: requires-python
25
+ Dynamic: summary
26
+
27
+ # Featrix Model Card - Python Package
28
+
29
+ Python package for rendering Featrix Model Card JSON to HTML or plain text.
30
+
31
+ **HTML rendering** delegates to the canonical JavaScript renderer loaded from CDN,
32
+ ensuring the Python output always matches the JS version.
33
+
34
+ **Text rendering** is a standalone Python implementation for terminal/log output.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install featrix-modelcard
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ### HTML Renderer
45
+
46
+ The HTML renderer generates a standalone page that loads `model-card.js` from the
47
+ Featrix CDN. The JavaScript renderer handles all layout, styling, and interactivity.
48
+
49
+ ```python
50
+ from featrix_modelcard import render_html, render_html_to_file
51
+
52
+ # Load model card JSON
53
+ import json
54
+ with open('model_card.json', 'r') as f:
55
+ model_card = json.load(f)
56
+
57
+ # Render to file
58
+ render_html_to_file(model_card, 'output.html')
59
+
60
+ # Render to string
61
+ html = render_html(model_card)
62
+
63
+ # With 3D sphere visualization
64
+ html = render_html(model_card, show_sphere=True)
65
+
66
+ # With explicit session ID for sphere
67
+ html = render_html(model_card, show_sphere=True, session_id='my-session-id')
68
+
69
+ # Override CDN URL (e.g. for local development)
70
+ html = render_html(model_card, cdn_url='http://localhost:8080/model-card.js')
71
+ ```
72
+
73
+ ### Text Renderer
74
+
75
+ ```python
76
+ from featrix_modelcard import render_brief_text, render_detailed_text, print_text
77
+
78
+ # Brief summary
79
+ print_text(model_card, detailed=False)
80
+
81
+ # Detailed output
82
+ print_text(model_card, detailed=True)
83
+
84
+ # Get as strings
85
+ brief = render_brief_text(model_card)
86
+ detailed = render_detailed_text(model_card)
87
+ ```
88
+
89
+ ## API Reference
90
+
91
+ ### HTML Functions
92
+
93
+ - `render_html(model_card_json, *, show_sphere=False, session_id=None, cdn_url=None)` -> `str`
94
+ - `render_html_to_file(model_card_json, output_path, *, show_sphere=False, session_id=None, cdn_url=None)` -> `str`
95
+ - `print_html(model_card_json, file=None, **kwargs)` -> `str`
96
+
97
+ ### Text Functions
98
+
99
+ - `render_brief_text(model_card_json)` -> `str`
100
+ - `render_detailed_text(model_card_json)` -> `str`
101
+ - `render_text_to_file(model_card_json, output_path, detailed=True)` -> `str`
102
+ - `print_text(model_card_json, detailed=True, file=None)` -> `str`
103
+
104
+ ## Architecture
105
+
106
+ The HTML renderer is a thin wrapper that embeds your model card JSON into a page
107
+ that loads the canonical `FeatrixModelCard` JavaScript renderer from the CDN.
108
+ This means:
109
+
110
+ - Python HTML output is always in sync with the JS version
111
+ - No rendering logic to maintain in Python
112
+ - All styling, interactivity, and features come from the JS renderer
113
+ - Zero external Python dependencies
114
+
115
+ The text renderer is a standalone Python implementation for use in terminals,
116
+ logs, and non-browser contexts.
@@ -0,0 +1,90 @@
1
+ # Featrix Model Card - Python Package
2
+
3
+ Python package for rendering Featrix Model Card JSON to HTML or plain text.
4
+
5
+ **HTML rendering** delegates to the canonical JavaScript renderer loaded from CDN,
6
+ ensuring the Python output always matches the JS version.
7
+
8
+ **Text rendering** is a standalone Python implementation for terminal/log output.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pip install featrix-modelcard
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ### HTML Renderer
19
+
20
+ The HTML renderer generates a standalone page that loads `model-card.js` from the
21
+ Featrix CDN. The JavaScript renderer handles all layout, styling, and interactivity.
22
+
23
+ ```python
24
+ from featrix_modelcard import render_html, render_html_to_file
25
+
26
+ # Load model card JSON
27
+ import json
28
+ with open('model_card.json', 'r') as f:
29
+ model_card = json.load(f)
30
+
31
+ # Render to file
32
+ render_html_to_file(model_card, 'output.html')
33
+
34
+ # Render to string
35
+ html = render_html(model_card)
36
+
37
+ # With 3D sphere visualization
38
+ html = render_html(model_card, show_sphere=True)
39
+
40
+ # With explicit session ID for sphere
41
+ html = render_html(model_card, show_sphere=True, session_id='my-session-id')
42
+
43
+ # Override CDN URL (e.g. for local development)
44
+ html = render_html(model_card, cdn_url='http://localhost:8080/model-card.js')
45
+ ```
46
+
47
+ ### Text Renderer
48
+
49
+ ```python
50
+ from featrix_modelcard import render_brief_text, render_detailed_text, print_text
51
+
52
+ # Brief summary
53
+ print_text(model_card, detailed=False)
54
+
55
+ # Detailed output
56
+ print_text(model_card, detailed=True)
57
+
58
+ # Get as strings
59
+ brief = render_brief_text(model_card)
60
+ detailed = render_detailed_text(model_card)
61
+ ```
62
+
63
+ ## API Reference
64
+
65
+ ### HTML Functions
66
+
67
+ - `render_html(model_card_json, *, show_sphere=False, session_id=None, cdn_url=None)` -> `str`
68
+ - `render_html_to_file(model_card_json, output_path, *, show_sphere=False, session_id=None, cdn_url=None)` -> `str`
69
+ - `print_html(model_card_json, file=None, **kwargs)` -> `str`
70
+
71
+ ### Text Functions
72
+
73
+ - `render_brief_text(model_card_json)` -> `str`
74
+ - `render_detailed_text(model_card_json)` -> `str`
75
+ - `render_text_to_file(model_card_json, output_path, detailed=True)` -> `str`
76
+ - `print_text(model_card_json, detailed=True, file=None)` -> `str`
77
+
78
+ ## Architecture
79
+
80
+ The HTML renderer is a thin wrapper that embeds your model card JSON into a page
81
+ that loads the canonical `FeatrixModelCard` JavaScript renderer from the CDN.
82
+ This means:
83
+
84
+ - Python HTML output is always in sync with the JS version
85
+ - No rendering logic to maintain in Python
86
+ - All styling, interactivity, and features come from the JS renderer
87
+ - Zero external Python dependencies
88
+
89
+ The text renderer is a standalone Python implementation for use in terminals,
90
+ logs, and non-browser contexts.
@@ -0,0 +1,67 @@
1
+ """
2
+ Featrix Model Card - Python Package
3
+
4
+ Render Featrix Model Card JSON to HTML or plain text.
5
+
6
+ HTML rendering delegates to the canonical JavaScript renderer (loaded from CDN),
7
+ ensuring the Python output always matches the JS version.
8
+
9
+ Text rendering is a standalone Python implementation for terminal/log output.
10
+ """
11
+
12
+ from .html_renderer import render_html, render_to_file as render_html_to_file
13
+ from .text_renderer import (
14
+ render_brief_text,
15
+ render_detailed_text,
16
+ render_to_file as render_text_to_file,
17
+ )
18
+
19
+ __version__ = "1.10"
20
+
21
+
22
+ def print_html(model_card_json, file=None, **kwargs):
23
+ """
24
+ Render model card to HTML and print it.
25
+
26
+ Args:
27
+ model_card_json: Model card JSON dictionary.
28
+ file: File-like object to print to (default: sys.stdout).
29
+ **kwargs: Passed to render_html (show_sphere, session_id, cdn_url).
30
+
31
+ Returns:
32
+ str: The rendered HTML string.
33
+ """
34
+ html = render_html(model_card_json, **kwargs)
35
+ print(html, file=file)
36
+ return html
37
+
38
+
39
+ def print_text(model_card_json, detailed=True, file=None):
40
+ """
41
+ Render model card to text and print it.
42
+
43
+ Args:
44
+ model_card_json: Model card JSON dictionary.
45
+ detailed: If True, render detailed version; if False, render brief version.
46
+ file: File-like object to print to (default: sys.stdout).
47
+
48
+ Returns:
49
+ str: The rendered text string.
50
+ """
51
+ if detailed:
52
+ text = render_detailed_text(model_card_json)
53
+ else:
54
+ text = render_brief_text(model_card_json)
55
+ print(text, file=file)
56
+ return text
57
+
58
+
59
+ __all__ = [
60
+ "render_html",
61
+ "render_html_to_file",
62
+ "render_brief_text",
63
+ "render_detailed_text",
64
+ "render_text_to_file",
65
+ "print_html",
66
+ "print_text",
67
+ ]
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ HTML renderer for Featrix Model Card JSON.
4
+
5
+ Generates a standalone HTML page that loads the canonical JavaScript renderer
6
+ from the CDN, ensuring the Python output always matches the JS version.
7
+ """
8
+
9
+ import json
10
+ from datetime import datetime
11
+ from typing import Any, Dict, Optional
12
+
13
+
14
+ CDN_BASE = "https://bits.featrix.com/js/featrix-modelcard"
15
+
16
+
17
+ def render_html(
18
+ model_card_json: Dict[str, Any],
19
+ *,
20
+ show_sphere: bool = False,
21
+ session_id: Optional[str] = None,
22
+ cdn_url: Optional[str] = None,
23
+ ) -> str:
24
+ """
25
+ Render a model card JSON dict to a standalone HTML page.
26
+
27
+ Uses the canonical FeatrixModelCard JavaScript renderer loaded from CDN,
28
+ so the output is always in sync with the JS version.
29
+
30
+ Args:
31
+ model_card_json: Model card JSON dictionary.
32
+ show_sphere: If True, enable the 3D sphere visualization thumbnail.
33
+ session_id: Explicit session ID for the sphere viewer. If not provided,
34
+ it will be resolved from the model card data.
35
+ cdn_url: Override the CDN URL for model-card.js.
36
+
37
+ Returns:
38
+ str: Complete standalone HTML document.
39
+ """
40
+ js_url = cdn_url or f"{CDN_BASE}/model-card.js"
41
+ model_name = (model_card_json.get("model_identification") or {}).get(
42
+ "name", "Model Card"
43
+ )
44
+
45
+ # Build the options object for renderHTML
46
+ options_parts = []
47
+ if show_sphere:
48
+ options_parts.append("showSphere: true")
49
+ if session_id:
50
+ options_parts.append(f'sessionId: {json.dumps(session_id)}')
51
+ options_js = "{" + ", ".join(options_parts) + "}" if options_parts else "{}"
52
+
53
+ json_str = json.dumps(model_card_json, indent=2)
54
+
55
+ return f"""<!DOCTYPE html>
56
+ <html lang="en">
57
+ <head>
58
+ <meta charset="UTF-8">
59
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
60
+ <title>Model Card - {_escape_html(model_name)}</title>
61
+ <style>
62
+ body {{
63
+ margin: 0;
64
+ padding: 20px;
65
+ background: #fff;
66
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
67
+ }}
68
+ </style>
69
+ </head>
70
+ <body>
71
+ <div id="model-card-container"></div>
72
+ <script src="{_escape_html(js_url)}"></script>
73
+ <script>
74
+ var modelCardJson = {json_str};
75
+ var options = {options_js};
76
+ var container = document.getElementById('model-card-container');
77
+ container.innerHTML = FeatrixModelCard.renderHTML(modelCardJson, options);
78
+ FeatrixModelCard.attachEventListeners(container);
79
+ </script>
80
+ </body>
81
+ </html>"""
82
+
83
+
84
+ def render_to_file(
85
+ model_card_json: Dict[str, Any],
86
+ output_path: str,
87
+ *,
88
+ show_sphere: bool = False,
89
+ session_id: Optional[str] = None,
90
+ cdn_url: Optional[str] = None,
91
+ ) -> str:
92
+ """
93
+ Render model card JSON to an HTML file.
94
+
95
+ Args:
96
+ model_card_json: Model card JSON dictionary.
97
+ output_path: Path to write the HTML file.
98
+ show_sphere: If True, enable the 3D sphere visualization.
99
+ session_id: Explicit session ID for the sphere viewer.
100
+ cdn_url: Override the CDN URL for model-card.js.
101
+
102
+ Returns:
103
+ str: The output_path that was written to.
104
+ """
105
+ html = render_html(
106
+ model_card_json,
107
+ show_sphere=show_sphere,
108
+ session_id=session_id,
109
+ cdn_url=cdn_url,
110
+ )
111
+ with open(output_path, "w", encoding="utf-8") as f:
112
+ f.write(html)
113
+ return output_path
114
+
115
+
116
+ def _escape_html(text: str) -> str:
117
+ """Escape HTML special characters."""
118
+ return (
119
+ text.replace("&", "&amp;")
120
+ .replace("<", "&lt;")
121
+ .replace(">", "&gt;")
122
+ .replace('"', "&quot;")
123
+ )
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Plain text renderer for Featrix Model Card JSON.
4
+
5
+ Provides both brief and detailed versions matching the current JSON schema
6
+ (model_identification, embedding_space, best_epochs, class_imbalance,
7
+ training_optimization, training_dataset, disk_usage).
8
+ """
9
+
10
+ import json
11
+ import re
12
+ from typing import Any, Dict, Optional
13
+
14
+
15
+ def format_value(value: Any, precision: int = 4) -> str:
16
+ """Format a value for display."""
17
+ if value is None:
18
+ return "N/A"
19
+ if isinstance(value, float):
20
+ return f"{value:.{precision}f}".rstrip("0").rstrip(".")
21
+ if isinstance(value, bool):
22
+ return str(value)
23
+ if isinstance(value, (list, dict)):
24
+ return json.dumps(value, indent=2)
25
+ return str(value)
26
+
27
+
28
+ def format_pct(value: Optional[float]) -> str:
29
+ """Format a 0-1 float as a percentage."""
30
+ if value is None:
31
+ return "N/A"
32
+ return f"{value * 100:.1f}%"
33
+
34
+
35
+ def format_large_number(value) -> str:
36
+ """Format large numbers (e.g. 264925317 -> 265.0M)."""
37
+ if value is None:
38
+ return "N/A"
39
+ if value >= 1_000_000_000:
40
+ return f"{value / 1_000_000_000:.1f}B"
41
+ if value >= 1_000_000:
42
+ return f"{value / 1_000_000:.1f}M"
43
+ if value >= 1_000:
44
+ return f"{value / 1_000:.1f}K"
45
+ return f"{value:,}"
46
+
47
+
48
+ def _map_model_type(mi: dict) -> str:
49
+ """Map model_type + target_column_type to display string."""
50
+ model_type = mi.get("model_type", "")
51
+ target_type = (mi.get("target_column_type") or "").lower()
52
+ mt = model_type.lower()
53
+
54
+ if mt in ("embedding space", "es"):
55
+ return "Foundational Embedding Space"
56
+ if mt in ("single predictor", "sp"):
57
+ if target_type == "set":
58
+ return "Binary Classifier"
59
+ if target_type == "scalar":
60
+ return "Regression"
61
+ return "Single Predictor"
62
+ return model_type or "N/A"
63
+
64
+
65
+ def _parse_model_path(path: Optional[str]) -> tuple:
66
+ """Extract session ID and job ID from best_model_path."""
67
+ if not path:
68
+ return None, None
69
+ parts = path.split("/")
70
+ session_id = None
71
+ job_id = None
72
+ for part in parts:
73
+ if part.startswith("predictor-"):
74
+ session_id = part[: len(part) - 37] if len(part) > 37 else part
75
+ if part.startswith("train_single_predictor_") or part.startswith("train_"):
76
+ job_id = part
77
+ return session_id, job_id
78
+
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # Brief renderer
82
+ # ---------------------------------------------------------------------------
83
+
84
+
85
+ def render_brief_text(data: Dict[str, Any]) -> str:
86
+ """Render a compact one-screen summary of the model card."""
87
+ mi = data.get("model_identification", {})
88
+ be = data.get("best_epochs", {})
89
+ ci = data.get("class_imbalance", {})
90
+ es = data.get("embedding_space", {})
91
+
92
+ model_name = mi.get("name", "Model Card")
93
+ model_type = _map_model_type(mi)
94
+ status = (mi.get("status") or "N/A").upper()
95
+ if status == "DONE":
96
+ status = "READY"
97
+
98
+ lines = [
99
+ f"MODEL CARD: {model_name}",
100
+ "=" * 60,
101
+ "",
102
+ f"Target: {mi.get('target_column', 'N/A')}",
103
+ f"Type: {model_type}",
104
+ f"Status: {status}",
105
+ f"Trained: {mi.get('training_date', 'N/A')}",
106
+ ]
107
+
108
+ # Best metrics
109
+ roc_auc = _get_metric_value(be, "best_roc_auc", "auc")
110
+ pr_auc = _get_metric_value(be, "best_pr_auc", "pr_auc")
111
+ f1 = _get_metric_value(be, "best_roc_auc", "f1")
112
+ acc = _get_metric_value(be, "best_roc_auc", "accuracy")
113
+
114
+ lines.append("")
115
+ lines.append(
116
+ f"Accuracy: {format_pct(acc)} "
117
+ f"AUC: {format_pct(roc_auc)} "
118
+ f"PR-AUC: {format_pct(pr_auc)} "
119
+ f"F1: {format_pct(f1)}"
120
+ )
121
+
122
+ # Class imbalance summary
123
+ if ci.get("total_samples"):
124
+ lines.append(
125
+ f"Samples: {ci['total_samples']:,} "
126
+ f"Imbalance: {ci.get('imbalance_ratio', 'N/A')}:1"
127
+ )
128
+
129
+ # Model stack summary
130
+ if es:
131
+ lines.append(
132
+ f"Foundation: {format_large_number(es.get('num_parameters'))} params, "
133
+ f"d_model={es.get('d_model', 'N/A')}"
134
+ )
135
+
136
+ lines.append("")
137
+ return "\n".join(lines)
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # Detailed renderer
142
+ # ---------------------------------------------------------------------------
143
+
144
+
145
+ def render_detailed_text(data: Dict[str, Any]) -> str:
146
+ """Render a full detailed text model card."""
147
+ sections = [
148
+ _render_model_identification(data),
149
+ _render_model_stack(data),
150
+ _render_best_epochs(data),
151
+ _render_training_optimization(data),
152
+ _render_training_dataset(data),
153
+ ]
154
+ return "\n".join(s for s in sections if s)
155
+
156
+
157
+ def _render_model_identification(data: dict) -> str:
158
+ mi = data.get("model_identification", {})
159
+ du = data.get("disk_usage", {})
160
+ es = data.get("embedding_space", {})
161
+ be = data.get("best_epochs", {})
162
+ ci = data.get("class_imbalance", {})
163
+
164
+ model_name = mi.get("name", "Model Card")
165
+ model_type = _map_model_type(mi)
166
+ status = (mi.get("status") or "N/A").upper()
167
+ if status == "DONE":
168
+ status = "READY"
169
+
170
+ session_id, job_id = _parse_model_path(du.get("best_model_path"))
171
+ model_id = session_id or (mi.get("session_id", "N/A")[:20] if mi.get("session_id") else "N/A")
172
+
173
+ framework = mi.get("framework", "N/A")
174
+ framework = re.sub(r"\s+unknown$", "", framework, flags=re.IGNORECASE).strip() or "N/A"
175
+
176
+ # Best metrics
177
+ roc_auc = _get_metric_value(be, "best_roc_auc", "auc")
178
+ pr_auc = _get_metric_value(be, "best_pr_auc", "pr_auc")
179
+
180
+ # PR-AUC lift
181
+ prevalence = None
182
+ if ci.get("minority_class_count") and ci.get("total_samples"):
183
+ prevalence = ci["minority_class_count"] / ci["total_samples"]
184
+ pr_auc_lift = (pr_auc / prevalence) if (pr_auc and prevalence) else None
185
+
186
+ lines = [
187
+ f"MODEL CARD: {model_name}",
188
+ "=" * 80,
189
+ "",
190
+ "MODEL IDENTIFICATION",
191
+ "-" * 60,
192
+ f" Target Column: {mi.get('target_column', 'N/A')}",
193
+ f" Model Type: {model_type}",
194
+ f" Best ROC-AUC: {format_pct(roc_auc)}",
195
+ f" Best PR-AUC: {format_pct(pr_auc)}"
196
+ + (f" [{pr_auc_lift:.1f}x lift]" if pr_auc_lift else ""),
197
+ "",
198
+ f" Status: {status}",
199
+ f" Training Date: {mi.get('training_date', 'N/A')}",
200
+ f" Model ID: {model_id}",
201
+ f" Cluster: {(mi.get('compute_cluster') or 'N/A').upper()}",
202
+ f" Dims: {es.get('d_model', 'N/A')}",
203
+ f" Framework: {framework}",
204
+ "",
205
+ ]
206
+ return "\n".join(lines)
207
+
208
+
209
+ def _render_model_stack(data: dict) -> str:
210
+ es = data.get("embedding_space")
211
+ if not es:
212
+ return ""
213
+
214
+ sp = data.get("single_predictor") or data.get("predictor") or {}
215
+ ma = data.get("model_architecture") or {}
216
+ ms = (data.get("model_stack") or [{}])[0] if data.get("model_stack") else {}
217
+ ci = data.get("class_imbalance") or {}
218
+
219
+ sp_rows = ci.get("total_samples") or ms.get("rows") or sp.get("num_rows", 0)
220
+ sp_layers = ms.get("layers") or ma.get("predictor_layers") or sp.get("num_layers", 0)
221
+ sp_params = ms.get("parameters") or ma.get("predictor_parameters") or sp.get("num_parameters", 0)
222
+
223
+ lines = [
224
+ "MODEL STACK",
225
+ "-" * 60,
226
+ f" {'':18s} {'Labeled':>8s} {'Rows':>10s} {'Layers':>10s} {'Parameters':>12s}",
227
+ f" {'Predictor':18s} {'Yes':>8s} {sp_rows:>10,} {format_large_number(sp_layers):>10s} {format_large_number(sp_params):>12s}",
228
+ f" {'Foundation':18s} {'No':>8s} {es.get('num_rows', 0):>10,} {format_large_number(es.get('num_layers')):>10s} {format_large_number(es.get('num_parameters')):>12s}",
229
+ "",
230
+ ]
231
+ return "\n".join(lines)
232
+
233
+
234
+ def _render_best_epochs(data: dict) -> str:
235
+ be = data.get("best_epochs")
236
+ if not be:
237
+ return ""
238
+
239
+ lines = [
240
+ "MODEL DETAILS",
241
+ "-" * 60,
242
+ ]
243
+
244
+ for label, key in [("Best ROC-AUC", "best_roc_auc"), ("Best PR-AUC", "best_pr_auc")]:
245
+ epoch_data = be.get(key)
246
+ if not epoch_data:
247
+ continue
248
+
249
+ cdm = epoch_data.get("classification_display_metadata") or {}
250
+ epoch_num = epoch_data.get("epoch") or cdm.get("epoch", "N/A")
251
+ metrics = cdm.get("classification_metrics") or {}
252
+
253
+ lines.append(f"\n {label} -- Epoch {epoch_num}")
254
+ lines.append(f" {'~' * 40}")
255
+
256
+ # Metrics table (top 4)
257
+ for mkey in ["accuracy", "auc", "pr_auc", "f1"]:
258
+ m = metrics.get(mkey)
259
+ if not m:
260
+ continue
261
+ val = format_pct(m.get("value"))
262
+ lines.append(f" {mkey.upper().replace('_', ' '):12s} {val}")
263
+
264
+ # Confusion matrix
265
+ cm = cdm.get("confusion_matrix")
266
+ if cm:
267
+ tp, fn, fp, tn = cm.get("tp", 0), cm.get("fn", 0), cm.get("fp", 0), cm.get("tn", 0)
268
+ total_pos = tp + fn
269
+ total_neg = tn + fp
270
+
271
+ lines.append("")
272
+ lines.append(" Confusion Matrix:")
273
+ lines.append(f" Pred Pos Pred Neg")
274
+ lines.append(f" Actual Pos {tp:>5d} {fn:>5d}")
275
+ lines.append(f" Actual Neg {fp:>5d} {tn:>5d}")
276
+
277
+ # Derived metrics
278
+ hit_rate = tp / total_pos if total_pos > 0 else 0
279
+ miss_rate = fn / total_pos if total_pos > 0 else 0
280
+ specificity = tn / total_neg if total_neg > 0 else 0
281
+ fpr = fp / total_neg if total_neg > 0 else 0
282
+ precision = tp / (tp + fp) if (tp + fp) > 0 else 0
283
+
284
+ lines.append("")
285
+ lines.append(f" Hit Rate (Recall): {hit_rate * 100:.1f}% TP/(TP+FN)")
286
+ lines.append(f" Miss Rate: {miss_rate * 100:.1f}% FN/(TP+FN)")
287
+ lines.append(f" Specificity (TNR): {specificity * 100:.1f}% TN/(TN+FP)")
288
+ lines.append(f" False Alarm (FPR): {fpr * 100:.1f}% FP/(TN+FP)")
289
+ lines.append(f" Precision (PPV): {precision * 100:.1f}% TP/(TP+FP)")
290
+
291
+ lines.append("")
292
+
293
+ return "\n".join(lines)
294
+
295
+
296
+ def _render_training_optimization(data: dict) -> str:
297
+ to = data.get("training_optimization")
298
+ if not to:
299
+ return ""
300
+
301
+ lines = [
302
+ "TRAINING OPTIMIZATION",
303
+ "-" * 60,
304
+ ]
305
+
306
+ if to.get("optimization_description"):
307
+ lines.append(f" Strategy: {to['optimization_description']}")
308
+ lines.append("")
309
+
310
+ lines.append(f" Loss Function: {to.get('loss_function', 'N/A')}")
311
+ lines.append(f" Optimization Priority: {(to.get('optimization_priority') or 'N/A').capitalize()}")
312
+
313
+ checkpoint = to.get("checkpoint_metric", "")
314
+ if checkpoint and checkpoint.lower() != "none":
315
+ lines.append(f" Checkpoint Metric: {checkpoint.upper().replace('_', '-')}")
316
+ else:
317
+ lines.append(f" Checkpoint Metric: Default")
318
+
319
+ if to.get("focal_gamma") is not None or to.get("focal_alpha") is not None:
320
+ lines.append(f" Focal Loss: gamma={to.get('focal_gamma', 'N/A')}, alpha={to.get('focal_alpha', 'N/A')}")
321
+
322
+ if to.get("class_weights"):
323
+ lines.append(f" Class Weights: [{', '.join(str(w) for w in to['class_weights'])}]")
324
+
325
+ cs = to.get("cost_sensitive")
326
+ if cs:
327
+ lines.append(f" Cost-Sensitive: FP={cs.get('cost_false_positive', 1.0)}, FN={cs.get('cost_false_negative', 1.0)}")
328
+
329
+ if to.get("adaptive_loss") is not None:
330
+ adj = f" ({to['gamma_adjustments']} adjustments)" if to.get("gamma_adjustments") else ""
331
+ lines.append(f" Adaptive Loss: {'Yes' if to['adaptive_loss'] else 'No'}{adj}")
332
+
333
+ if to.get("checkpoint_value") is not None:
334
+ lines.append(f" Best Checkpoint: {to['checkpoint_value'] * 100:.2f}% at epoch {to.get('checkpoint_epoch', 'N/A')}")
335
+
336
+ if to.get("positive_class") is not None:
337
+ lines.append(f" Positive Class: \"{to['positive_class']}\"")
338
+
339
+ lines.append("")
340
+ return "\n".join(lines)
341
+
342
+
343
+ def _render_training_dataset(data: dict) -> str:
344
+ ci = data.get("class_imbalance") or {}
345
+ td = data.get("training_dataset") or {}
346
+
347
+ if not ci and not td:
348
+ return ""
349
+
350
+ lines = [
351
+ "TRAINING DATASET",
352
+ "-" * 60,
353
+ ]
354
+
355
+ if ci.get("train_distribution") or ci.get("class_distribution"):
356
+ train0 = (ci.get("train_distribution") or {}).get("0", 0)
357
+ train1 = (ci.get("train_distribution") or {}).get("1", 0)
358
+ val0 = (ci.get("val_distribution") or {}).get("0", 0)
359
+ val1 = (ci.get("val_distribution") or {}).get("1", 0)
360
+ total_train = train0 + train1
361
+ total_val = val0 + val1
362
+ total = ci.get("total_samples") or td.get("train_rows") or (total_train + total_val)
363
+
364
+ minority = ci.get("minority_class", "1")
365
+ majority = ci.get("majority_class", "0")
366
+
367
+ lines.append(f" {'':12s} Class \"{minority}\" Class \"{majority}\" Total")
368
+ lines.append(f" {'Train':12s} {train1:>10,} {train0:>12,} {total_train:>8,}")
369
+ lines.append(f" {'Validation':12s} {val1:>10,} {val0:>12,} {total_val:>8,}")
370
+ lines.append(f" {'Total':12s} {ci.get('minority_class_count', train1 + val1):>10,} {ci.get('majority_class_count', train0 + val0):>12,} {total:>8,}")
371
+ lines.append("")
372
+
373
+ if ci.get("imbalance_ratio"):
374
+ minority_pct = (ci.get("minority_class_count", 0) / total * 100) if total else 0
375
+ lines.append(f" Imbalance ratio: {ci['imbalance_ratio']}:1 (minority is {minority_pct:.1f}% of data)")
376
+ elif td.get("train_rows"):
377
+ lines.append(f" Training rows: {td['train_rows']:,}")
378
+
379
+ lines.append("")
380
+ return "\n".join(lines)
381
+
382
+
383
+ # ---------------------------------------------------------------------------
384
+ # Helpers
385
+ # ---------------------------------------------------------------------------
386
+
387
+
388
+ def _get_metric_value(best_epochs: dict, epoch_key: str, metric_key: str):
389
+ """Extract a metric value from best_epochs nested structure."""
390
+ epoch = best_epochs.get(epoch_key) or {}
391
+ cdm = epoch.get("classification_display_metadata") or {}
392
+ metrics = cdm.get("classification_metrics") or {}
393
+ m = metrics.get(metric_key) or {}
394
+ return m.get("value")
395
+
396
+
397
+ def render_to_file(
398
+ model_card_json: Dict[str, Any],
399
+ output_path: str,
400
+ detailed: bool = True,
401
+ ) -> str:
402
+ """Render model card JSON to text file."""
403
+ text = render_detailed_text(model_card_json) if detailed else render_brief_text(model_card_json)
404
+ with open(output_path, "w", encoding="utf-8") as f:
405
+ f.write(text)
406
+ return output_path
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: featrix-modelcard
3
+ Version: 1.10
4
+ Summary: Render Featrix Sphere Model Card JSON to HTML or plain text
5
+ Home-page: https://github.com/featrix/model-card
6
+ Author: Featrix
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.7
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Requires-Python: >=3.7
18
+ Description-Content-Type: text/markdown
19
+ Dynamic: author
20
+ Dynamic: classifier
21
+ Dynamic: description
22
+ Dynamic: description-content-type
23
+ Dynamic: home-page
24
+ Dynamic: requires-python
25
+ Dynamic: summary
26
+
27
+ # Featrix Model Card - Python Package
28
+
29
+ Python package for rendering Featrix Model Card JSON to HTML or plain text.
30
+
31
+ **HTML rendering** delegates to the canonical JavaScript renderer loaded from CDN,
32
+ ensuring the Python output always matches the JS version.
33
+
34
+ **Text rendering** is a standalone Python implementation for terminal/log output.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install featrix-modelcard
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ### HTML Renderer
45
+
46
+ The HTML renderer generates a standalone page that loads `model-card.js` from the
47
+ Featrix CDN. The JavaScript renderer handles all layout, styling, and interactivity.
48
+
49
+ ```python
50
+ from featrix_modelcard import render_html, render_html_to_file
51
+
52
+ # Load model card JSON
53
+ import json
54
+ with open('model_card.json', 'r') as f:
55
+ model_card = json.load(f)
56
+
57
+ # Render to file
58
+ render_html_to_file(model_card, 'output.html')
59
+
60
+ # Render to string
61
+ html = render_html(model_card)
62
+
63
+ # With 3D sphere visualization
64
+ html = render_html(model_card, show_sphere=True)
65
+
66
+ # With explicit session ID for sphere
67
+ html = render_html(model_card, show_sphere=True, session_id='my-session-id')
68
+
69
+ # Override CDN URL (e.g. for local development)
70
+ html = render_html(model_card, cdn_url='http://localhost:8080/model-card.js')
71
+ ```
72
+
73
+ ### Text Renderer
74
+
75
+ ```python
76
+ from featrix_modelcard import render_brief_text, render_detailed_text, print_text
77
+
78
+ # Brief summary
79
+ print_text(model_card, detailed=False)
80
+
81
+ # Detailed output
82
+ print_text(model_card, detailed=True)
83
+
84
+ # Get as strings
85
+ brief = render_brief_text(model_card)
86
+ detailed = render_detailed_text(model_card)
87
+ ```
88
+
89
+ ## API Reference
90
+
91
+ ### HTML Functions
92
+
93
+ - `render_html(model_card_json, *, show_sphere=False, session_id=None, cdn_url=None)` -> `str`
94
+ - `render_html_to_file(model_card_json, output_path, *, show_sphere=False, session_id=None, cdn_url=None)` -> `str`
95
+ - `print_html(model_card_json, file=None, **kwargs)` -> `str`
96
+
97
+ ### Text Functions
98
+
99
+ - `render_brief_text(model_card_json)` -> `str`
100
+ - `render_detailed_text(model_card_json)` -> `str`
101
+ - `render_text_to_file(model_card_json, output_path, detailed=True)` -> `str`
102
+ - `print_text(model_card_json, detailed=True, file=None)` -> `str`
103
+
104
+ ## Architecture
105
+
106
+ The HTML renderer is a thin wrapper that embeds your model card JSON into a page
107
+ that loads the canonical `FeatrixModelCard` JavaScript renderer from the CDN.
108
+ This means:
109
+
110
+ - Python HTML output is always in sync with the JS version
111
+ - No rendering logic to maintain in Python
112
+ - All styling, interactivity, and features come from the JS renderer
113
+ - Zero external Python dependencies
114
+
115
+ The text renderer is a standalone Python implementation for use in terminals,
116
+ logs, and non-browser contexts.
@@ -0,0 +1,10 @@
1
+ MANIFEST.in
2
+ README.md
3
+ setup.py
4
+ featrix_modelcard/__init__.py
5
+ featrix_modelcard/html_renderer.py
6
+ featrix_modelcard/text_renderer.py
7
+ featrix_modelcard.egg-info/PKG-INFO
8
+ featrix_modelcard.egg-info/SOURCES.txt
9
+ featrix_modelcard.egg-info/dependency_links.txt
10
+ featrix_modelcard.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ featrix_modelcard
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,30 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ with open("README.md", "r", encoding="utf-8") as fh:
4
+ long_description = fh.read()
5
+
6
+ setup(
7
+ name="featrix-modelcard",
8
+ version="1.10",
9
+ author="Featrix",
10
+ description="Render Featrix Sphere Model Card JSON to HTML or plain text",
11
+ long_description=long_description,
12
+ long_description_content_type="text/markdown",
13
+ url="https://github.com/featrix/model-card",
14
+ packages=find_packages(),
15
+ classifiers=[
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Topic :: Software Development :: Libraries :: Python Modules",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.7",
21
+ "Programming Language :: Python :: 3.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "License :: OSI Approved :: MIT License",
26
+ ],
27
+ python_requires=">=3.7",
28
+ install_requires=[],
29
+ )
30
+