rpx_benchmark.paper_tracking_analysis
Paper-level statistical analysis for RPX D3 tracking cells.
1"""Paper-level statistical analysis for RPX D3 tracking cells.""" 2 3from __future__ import annotations 4 5import hashlib 6import json 7from pathlib import Path 8from typing import Any, Mapping, Sequence 9 10import numpy as np 11 12from .exceptions import ConfigError, DatasetError 13from .metrics.tracking_paper import PAPER_TRACKING_METRICS 14from .paper_depth_analysis import ( 15 DIFFICULTIES, 16 PHASES, 17 _holm_adjust, 18 _hotelling_transition, 19 _interaction_test, 20 _nested_effect, 21 _phase_contrasts, 22) 23 24TRACKING_DIRECTIONS = { 25 "hota": "higher", 26 "deta": "higher", 27 "assa": "higher", 28 "idf1": "higher", 29 "mota": "higher", 30 "idsw": "lower", 31} 32EGO_SPLIT_CELLS = {"easy": 33, "medium": 33, "hard": 34} 33_PHASE_NAMES = {0: "clutter", 1: "interaction", 2: "clean"} 34 35 36def _phase_test(cube: np.ndarray) -> dict[str, float]: 37 n_scenes, n_phases, n_metrics = cube.shape 38 if n_phases != 3 or n_metrics != len(PAPER_TRACKING_METRICS): 39 raise ConfigError(f"Unexpected D3 MANOVA cube shape: {cube.shape}") 40 responses = cube.reshape(n_scenes * n_phases, n_metrics) 41 subjects = np.repeat(np.eye(n_scenes), n_phases, axis=0) 42 phase = np.tile(_phase_contrasts(), (n_scenes, 1)) 43 return _nested_effect( 44 responses, 45 subjects, 46 np.column_stack((subjects, phase)), 47 df_h=2, 48 ) 49 50 51def _load_bounds(path: Path | None) -> tuple[dict[str, Any] | None, str | None]: 52 if path is None: 53 return None, None 54 raw = path.read_bytes() 55 value = json.loads(raw) 56 if not value.get("version") or not value.get("provenance"): 57 raise ConfigError("D3 JEDI bounds require version and provenance.") 58 epsilon = value.get("epsilon") 59 if not isinstance(epsilon, (int, float)) or not 0 < float(epsilon) <= 1: 60 raise ConfigError("D3 JEDI epsilon must be in (0, 1].") 61 metrics = value.get("metrics") 62 if not isinstance(metrics, dict) or set(metrics) != set(PAPER_TRACKING_METRICS): 63 raise ConfigError( 64 f"D3 JEDI bounds must define exactly {list(PAPER_TRACKING_METRICS)}." 65 ) 66 for metric, direction in TRACKING_DIRECTIONS.items(): 67 spec = metrics[metric] 68 if spec.get("direction") != direction: 69 raise ConfigError(f"D3 JEDI direction mismatch for {metric}.") 70 best, worst = spec.get("best"), spec.get("worst") 71 if not isinstance(best, (int, float)) or not isinstance(worst, (int, float)): 72 raise ConfigError(f"D3 JEDI bounds for {metric} must be numeric.") 73 if not np.isfinite([best, worst]).all(): 74 raise ConfigError(f"D3 JEDI bounds for {metric} must be finite.") 75 if (direction == "higher" and best <= worst) or ( 76 direction == "lower" and best >= worst 77 ): 78 raise ConfigError(f"D3 JEDI bounds contradict direction for {metric}.") 79 return value, hashlib.sha256(raw).hexdigest() 80 81 82def _jedi(values: Mapping[str, float], bounds: Mapping[str, Any]) -> dict[str, Any]: 83 desirabilities: dict[str, float] = {} 84 for metric in PAPER_TRACKING_METRICS: 85 spec = bounds["metrics"][metric] 86 desirability = (values[metric] - spec["worst"]) / (spec["best"] - spec["worst"]) 87 desirabilities[metric] = float(np.clip(desirability, 0, 1)) 88 epsilon = float(bounds["epsilon"]) 89 score = float( 90 np.exp(np.mean(np.log([max(value, epsilon) for value in desirabilities.values()]))) 91 ) 92 return {"score": score, "desirabilities": desirabilities} 93 94 95def analyze_tracking_cells( 96 paths: Sequence[str | Path], 97 *, 98 jedi_bounds_path: str | Path | None = None, 99) -> tuple[dict[str, Any], "Any"]: 100 import pandas as pd 101 102 frames = [pd.read_parquet(path) for path in paths] 103 cells = pd.concat(frames, ignore_index=True) 104 required = { 105 "model", 106 "task", 107 "split", 108 "scene", 109 "phase", 110 *(f"metric:{metric}" for metric in PAPER_TRACKING_METRICS), 111 } 112 missing = required - set(cells.columns) 113 if missing: 114 raise DatasetError(f"D3 cells are missing columns: {sorted(missing)}") 115 cells = cells[cells["task"] == "object_tracking"].copy() 116 if cells.empty: 117 raise DatasetError("No object_tracking cells found.") 118 models = set(cells["model"].astype(str)) 119 if len(models) != 1: 120 raise DatasetError(f"D3 analysis requires one model, got {sorted(models)}.") 121 if cells.duplicated(["scene", "phase"]).any(): 122 raise DatasetError("D3 cells contain duplicate scene-phase rows.") 123 if len(cells) != 300 or cells["scene"].nunique() != 100: 124 raise DatasetError( 125 f"D3 analysis requires 300 cells/100 scenes; got {len(cells)}/" 126 f"{cells['scene'].nunique()}." 127 ) 128 129 scenes = sorted(cells["scene"].astype(str).unique()) 130 raw = np.empty((100, 3, len(PAPER_TRACKING_METRICS)), dtype=np.float64) 131 difficulties: list[str] = [] 132 for scene_index, scene in enumerate(scenes): 133 scene_rows = cells[cells["scene"].astype(str) == scene] 134 tiers = set(scene_rows["split"].astype(str)) 135 if len(tiers) != 1 or next(iter(tiers)) not in DIFFICULTIES: 136 raise DatasetError(f"Invalid/changing difficulty for {scene}: {tiers}") 137 difficulties.append(next(iter(tiers))) 138 for phase in range(3): 139 phase_rows = scene_rows[scene_rows["phase"].astype(int) == phase] 140 if len(phase_rows) != 1: 141 raise DatasetError(f"Missing D3 cell for {scene}/phase {phase}.") 142 row = phase_rows.iloc[0] 143 for metric_index, metric in enumerate(PAPER_TRACKING_METRICS): 144 value = float(row[f"metric:{metric}"]) 145 if not np.isfinite(value): 146 raise DatasetError(f"Non-finite {metric} for {scene}/phase {phase}.") 147 raw[scene_index, phase, metric_index] = value 148 149 oriented = raw.copy() 150 for index, metric in enumerate(PAPER_TRACKING_METRICS): 151 if TRACKING_DIRECTIONS[metric] == "lower": 152 oriented[:, :, index] *= -1 153 flat = oriented.reshape(-1, len(PAPER_TRACKING_METRICS)) 154 standard_deviation = flat.std(axis=0, ddof=0) 155 if np.any(standard_deviation <= 0): 156 raise DatasetError( 157 f"D3 analysis requires {len(PAPER_TRACKING_METRICS)} non-constant metrics." 158 ) 159 standardized = ((flat - flat.mean(axis=0)) / standard_deviation).reshape(oriented.shape) 160 161 transitions: list[dict[str, Any]] = [] 162 for label, first, second in ( 163 ("clutter_to_interaction", 0, 1), 164 ("interaction_to_clean", 1, 2), 165 ("clutter_to_clean", 0, 2), 166 ): 167 transitions.append( 168 { 169 "transition": label, 170 **_hotelling_transition(standardized[:, first], standardized[:, second]), 171 } 172 ) 173 adjusted = _holm_adjust([value["p_value_raw"] for value in transitions]) 174 for transition, adjusted_p in zip(transitions, adjusted, strict=True): 175 transition["p_value_holm"] = adjusted_p 176 177 tier_array = np.asarray(difficulties) 178 per_tier = { 179 tier: { 180 "n_scenes": int(np.sum(tier_array == tier)), 181 **_phase_test(standardized[tier_array == tier]), 182 } 183 for tier in DIFFICULTIES 184 } 185 phase_means = { 186 _PHASE_NAMES[phase]: { 187 metric: float(raw[:, phase, index].mean()) 188 for index, metric in enumerate(PAPER_TRACKING_METRICS) 189 } 190 for phase in range(3) 191 } 192 overall_means = { 193 metric: float(raw[:, :, index].mean()) 194 for index, metric in enumerate(PAPER_TRACKING_METRICS) 195 } 196 bounds, bounds_sha = _load_bounds(Path(jedi_bounds_path) if jedi_bounds_path else None) 197 if bounds is None: 198 jedi: dict[str, Any] = {"status": "bounds_missing"} 199 else: 200 jedi = { 201 "status": "computed", 202 "bounds_version": bounds["version"], 203 "bounds_sha256": bounds_sha, 204 "overall": _jedi(overall_means, bounds), 205 "per_phase": { 206 phase: _jedi(values, bounds) for phase, values in phase_means.items() 207 }, 208 } 209 210 analysis = { 211 "schema_version": "rpx-d3-paper-analysis-v1", 212 "model": next(iter(models)), 213 "metrics": list(PAPER_TRACKING_METRICS), 214 "directions": TRACKING_DIRECTIONS, 215 "n_scenes": 100, 216 "n_cells": 300, 217 "overall_phase_manova": _phase_test(standardized), 218 "transitions": transitions, 219 "per_tier": per_tier, 220 "phase_by_difficulty": _interaction_test(standardized, difficulties), 221 "metric_means": {"overall": overall_means, "per_phase": phase_means}, 222 "jedi": jedi, 223 "jedi_status": jedi["status"], 224 } 225 return analysis, cells 226 227 228def analyze_ego_tracking_cells( 229 paths: Sequence[str | Path], 230) -> tuple[dict[str, Any], "Any"]: 231 """Aggregate the one-clip-per-scene RPX ego tracking protocol.""" 232 233 import pandas as pd 234 235 cells = pd.concat([pd.read_parquet(path) for path in paths], ignore_index=True) 236 required = { 237 "model", 238 "task", 239 "split", 240 "scene", 241 "phase", 242 *(f"metric:{metric}" for metric in PAPER_TRACKING_METRICS), 243 } 244 missing = required - set(cells.columns) 245 if missing: 246 raise DatasetError(f"Ego tracking cells are missing columns: {sorted(missing)}") 247 cells = cells[cells["task"] == "object_tracking"].copy() 248 if "dataset_protocol" in cells: 249 cells = cells[cells["dataset_protocol"] == "ego"].copy() 250 if cells.empty: 251 raise DatasetError("No ego object_tracking cells found.") 252 models = set(cells["model"].astype(str)) 253 if len(models) != 1: 254 raise DatasetError( 255 f"Ego tracking analysis requires one model, got {sorted(models)}." 256 ) 257 if cells.duplicated(["scene", "phase"]).any(): 258 raise DatasetError("Ego tracking cells contain duplicate scene-phase rows.") 259 split_counts = cells.groupby("split").size().to_dict() 260 if split_counts != EGO_SPLIT_CELLS or cells["scene"].nunique() != 100: 261 raise DatasetError( 262 "Ego analysis requires 100 scenes split 33/33/34; got " 263 f"{split_counts} across {cells['scene'].nunique()} scenes." 264 ) 265 266 metric_columns = {metric: f"metric:{metric}" for metric in PAPER_TRACKING_METRICS} 267 for metric, column in metric_columns.items(): 268 values = cells[column].astype(float).to_numpy() 269 if not np.isfinite(values).all(): 270 raise DatasetError(f"Non-finite ego tracking metric: {metric}.") 271 metric_means = { 272 "overall": { 273 metric: float(cells[column].astype(float).mean()) 274 for metric, column in metric_columns.items() 275 }, 276 "per_difficulty": { 277 split: { 278 metric: float(rows[column].astype(float).mean()) 279 for metric, column in metric_columns.items() 280 } 281 for split, rows in cells.groupby("split", sort=True) 282 }, 283 } 284 hardware_columns = ( 285 "latency_ms", 286 "throughput_fps", 287 "peak_gpu_memory_allocated_mb", 288 "peak_gpu_memory_reserved_mb", 289 "clip_wall_time_s", 290 "parameter_count", 291 ) 292 hardware_means = { 293 column: float(cells[column].astype(float).mean()) 294 for column in hardware_columns 295 if column in cells and np.isfinite(cells[column].astype(float)).all() 296 } 297 analysis = { 298 "schema_version": "rpx-ego-tracking-analysis-v1", 299 "dataset_protocol": "ego", 300 "model": next(iter(models)), 301 "metrics": list(PAPER_TRACKING_METRICS), 302 "directions": TRACKING_DIRECTIONS, 303 "n_scenes": 100, 304 "n_cells": len(cells), 305 "split_cells": split_counts, 306 "metric_means": metric_means, 307 "hardware_means": hardware_means, 308 } 309 return analysis, cells 310 311 312def write_ego_tracking_analysis( 313 analysis: Mapping[str, Any], 314 cells: Any, 315 output_dir: str | Path, 316) -> dict[str, Path]: 317 import pandas as pd 318 319 output = Path(output_dir) 320 output.mkdir(parents=True, exist_ok=True) 321 paths = { 322 "combined_cells": output / "combined_cells.parquet", 323 "json": output / "paper_analysis.json", 324 "markdown": output / "paper_analysis.md", 325 "table": output / "paper_table.csv", 326 } 327 cells.to_parquet(paths["combined_cells"], index=False) 328 paths["json"].write_text( 329 json.dumps(dict(analysis), indent=2, sort_keys=True) + "\n", 330 encoding="utf-8", 331 ) 332 rows = [] 333 for split, values in ( 334 [("overall", analysis["metric_means"]["overall"])] 335 + list(analysis["metric_means"]["per_difficulty"].items()) 336 ): 337 rows.append({"model": analysis["model"], "split": split, **values}) 338 pd.DataFrame(rows).to_csv(paths["table"], index=False) 339 lines = [ 340 "# RPX ego tracking analysis", 341 "", 342 f"- Model: `{analysis['model']}`", 343 f"- Clips/scenes: {analysis['n_cells']}", 344 "- Protocol: one ego video per scene", 345 "", 346 "| Split | HOTA | DetA | AssA | IDF1 | MOTA | ID switches |", 347 "|---|---:|---:|---:|---:|---:|---:|", 348 ] 349 for row in rows: 350 lines.append( 351 f"| {row['split']} | {row['hota']:.6g} | {row['deta']:.6g} | " 352 f"{row['assa']:.6g} | {row['idf1']:.6g} | " 353 f"{row['mota']:.6g} | {row['idsw']:.6g} |" 354 ) 355 paths["markdown"].write_text("\n".join(lines) + "\n", encoding="utf-8") 356 return paths 357 358 359def write_tracking_analysis( 360 analysis: Mapping[str, Any], 361 cells: Any, 362 output_dir: str | Path, 363) -> dict[str, Path]: 364 import pandas as pd 365 366 output = Path(output_dir) 367 output.mkdir(parents=True, exist_ok=True) 368 paths = { 369 "combined_cells": output / "combined_cells.parquet", 370 "json": output / "paper_analysis.json", 371 "markdown": output / "paper_analysis.md", 372 "table": output / "paper_table.csv", 373 } 374 cells.to_parquet(paths["combined_cells"], index=False) 375 paths["json"].write_text( 376 json.dumps(dict(analysis), indent=2, sort_keys=True) + "\n", 377 encoding="utf-8", 378 ) 379 overall = analysis["overall_phase_manova"] 380 table = { 381 "model": analysis["model"], 382 "phi": overall["phi_wilks"], 383 "wilks_lambda": overall["wilks_lambda"], 384 "p_value": overall["p_value"], 385 "partial_eta_squared": overall["partial_eta_squared"], 386 "jedi_status": analysis["jedi_status"], 387 } 388 pd.DataFrame([table]).to_csv(paths["table"], index=False) 389 lines = [ 390 "# RPX D3 paper analysis", 391 "", 392 f"- Model: `{analysis['model']}`", 393 f"- Cells: {analysis['n_cells']} (100 scenes × 3 phases)", 394 f"- Φ: {overall['phi_wilks']:.6g}", 395 f"- Wilks’ Λ: {overall['wilks_lambda']:.6g}", 396 f"- Rao p: {overall['p_value']:.6g}", 397 f"- Partial η²: {overall['partial_eta_squared']:.6g}", 398 f"- JEDI: `{analysis['jedi_status']}`", 399 "", 400 "## Per-phase means", 401 "", 402 "| Phase | HOTA | DetA | AssA | IDF1 | MOTA | ID switches |", 403 "|---|---:|---:|---:|---:|---:|---:|", 404 ] 405 for phase in PHASES: 406 values = analysis["metric_means"]["per_phase"][phase] 407 lines.append( 408 f"| {phase} | {values['hota']:.6g} | {values['deta']:.6g} | " 409 f"{values['assa']:.6g} | {values['idf1']:.6g} | " 410 f"{values['mota']:.6g} | {values['idsw']:.6g} |" 411 ) 412 paths["markdown"].write_text("\n".join(lines) + "\n", encoding="utf-8") 413 return paths
TRACKING_DIRECTIONS =
{'hota': 'higher', 'deta': 'higher', 'assa': 'higher', 'idf1': 'higher', 'mota': 'higher', 'idsw': 'lower'}
EGO_SPLIT_CELLS =
{'easy': 33, 'medium': 33, 'hard': 34}
def
analyze_tracking_cells( paths: Sequence[str | pathlib.Path], *, jedi_bounds_path: str | pathlib.Path | None = None) -> tuple[dict[str, typing.Any], typing.Any]:
96def analyze_tracking_cells( 97 paths: Sequence[str | Path], 98 *, 99 jedi_bounds_path: str | Path | None = None, 100) -> tuple[dict[str, Any], "Any"]: 101 import pandas as pd 102 103 frames = [pd.read_parquet(path) for path in paths] 104 cells = pd.concat(frames, ignore_index=True) 105 required = { 106 "model", 107 "task", 108 "split", 109 "scene", 110 "phase", 111 *(f"metric:{metric}" for metric in PAPER_TRACKING_METRICS), 112 } 113 missing = required - set(cells.columns) 114 if missing: 115 raise DatasetError(f"D3 cells are missing columns: {sorted(missing)}") 116 cells = cells[cells["task"] == "object_tracking"].copy() 117 if cells.empty: 118 raise DatasetError("No object_tracking cells found.") 119 models = set(cells["model"].astype(str)) 120 if len(models) != 1: 121 raise DatasetError(f"D3 analysis requires one model, got {sorted(models)}.") 122 if cells.duplicated(["scene", "phase"]).any(): 123 raise DatasetError("D3 cells contain duplicate scene-phase rows.") 124 if len(cells) != 300 or cells["scene"].nunique() != 100: 125 raise DatasetError( 126 f"D3 analysis requires 300 cells/100 scenes; got {len(cells)}/" 127 f"{cells['scene'].nunique()}." 128 ) 129 130 scenes = sorted(cells["scene"].astype(str).unique()) 131 raw = np.empty((100, 3, len(PAPER_TRACKING_METRICS)), dtype=np.float64) 132 difficulties: list[str] = [] 133 for scene_index, scene in enumerate(scenes): 134 scene_rows = cells[cells["scene"].astype(str) == scene] 135 tiers = set(scene_rows["split"].astype(str)) 136 if len(tiers) != 1 or next(iter(tiers)) not in DIFFICULTIES: 137 raise DatasetError(f"Invalid/changing difficulty for {scene}: {tiers}") 138 difficulties.append(next(iter(tiers))) 139 for phase in range(3): 140 phase_rows = scene_rows[scene_rows["phase"].astype(int) == phase] 141 if len(phase_rows) != 1: 142 raise DatasetError(f"Missing D3 cell for {scene}/phase {phase}.") 143 row = phase_rows.iloc[0] 144 for metric_index, metric in enumerate(PAPER_TRACKING_METRICS): 145 value = float(row[f"metric:{metric}"]) 146 if not np.isfinite(value): 147 raise DatasetError(f"Non-finite {metric} for {scene}/phase {phase}.") 148 raw[scene_index, phase, metric_index] = value 149 150 oriented = raw.copy() 151 for index, metric in enumerate(PAPER_TRACKING_METRICS): 152 if TRACKING_DIRECTIONS[metric] == "lower": 153 oriented[:, :, index] *= -1 154 flat = oriented.reshape(-1, len(PAPER_TRACKING_METRICS)) 155 standard_deviation = flat.std(axis=0, ddof=0) 156 if np.any(standard_deviation <= 0): 157 raise DatasetError( 158 f"D3 analysis requires {len(PAPER_TRACKING_METRICS)} non-constant metrics." 159 ) 160 standardized = ((flat - flat.mean(axis=0)) / standard_deviation).reshape(oriented.shape) 161 162 transitions: list[dict[str, Any]] = [] 163 for label, first, second in ( 164 ("clutter_to_interaction", 0, 1), 165 ("interaction_to_clean", 1, 2), 166 ("clutter_to_clean", 0, 2), 167 ): 168 transitions.append( 169 { 170 "transition": label, 171 **_hotelling_transition(standardized[:, first], standardized[:, second]), 172 } 173 ) 174 adjusted = _holm_adjust([value["p_value_raw"] for value in transitions]) 175 for transition, adjusted_p in zip(transitions, adjusted, strict=True): 176 transition["p_value_holm"] = adjusted_p 177 178 tier_array = np.asarray(difficulties) 179 per_tier = { 180 tier: { 181 "n_scenes": int(np.sum(tier_array == tier)), 182 **_phase_test(standardized[tier_array == tier]), 183 } 184 for tier in DIFFICULTIES 185 } 186 phase_means = { 187 _PHASE_NAMES[phase]: { 188 metric: float(raw[:, phase, index].mean()) 189 for index, metric in enumerate(PAPER_TRACKING_METRICS) 190 } 191 for phase in range(3) 192 } 193 overall_means = { 194 metric: float(raw[:, :, index].mean()) 195 for index, metric in enumerate(PAPER_TRACKING_METRICS) 196 } 197 bounds, bounds_sha = _load_bounds(Path(jedi_bounds_path) if jedi_bounds_path else None) 198 if bounds is None: 199 jedi: dict[str, Any] = {"status": "bounds_missing"} 200 else: 201 jedi = { 202 "status": "computed", 203 "bounds_version": bounds["version"], 204 "bounds_sha256": bounds_sha, 205 "overall": _jedi(overall_means, bounds), 206 "per_phase": { 207 phase: _jedi(values, bounds) for phase, values in phase_means.items() 208 }, 209 } 210 211 analysis = { 212 "schema_version": "rpx-d3-paper-analysis-v1", 213 "model": next(iter(models)), 214 "metrics": list(PAPER_TRACKING_METRICS), 215 "directions": TRACKING_DIRECTIONS, 216 "n_scenes": 100, 217 "n_cells": 300, 218 "overall_phase_manova": _phase_test(standardized), 219 "transitions": transitions, 220 "per_tier": per_tier, 221 "phase_by_difficulty": _interaction_test(standardized, difficulties), 222 "metric_means": {"overall": overall_means, "per_phase": phase_means}, 223 "jedi": jedi, 224 "jedi_status": jedi["status"], 225 } 226 return analysis, cells
def
analyze_ego_tracking_cells( paths: Sequence[str | pathlib.Path]) -> tuple[dict[str, typing.Any], typing.Any]:
229def analyze_ego_tracking_cells( 230 paths: Sequence[str | Path], 231) -> tuple[dict[str, Any], "Any"]: 232 """Aggregate the one-clip-per-scene RPX ego tracking protocol.""" 233 234 import pandas as pd 235 236 cells = pd.concat([pd.read_parquet(path) for path in paths], ignore_index=True) 237 required = { 238 "model", 239 "task", 240 "split", 241 "scene", 242 "phase", 243 *(f"metric:{metric}" for metric in PAPER_TRACKING_METRICS), 244 } 245 missing = required - set(cells.columns) 246 if missing: 247 raise DatasetError(f"Ego tracking cells are missing columns: {sorted(missing)}") 248 cells = cells[cells["task"] == "object_tracking"].copy() 249 if "dataset_protocol" in cells: 250 cells = cells[cells["dataset_protocol"] == "ego"].copy() 251 if cells.empty: 252 raise DatasetError("No ego object_tracking cells found.") 253 models = set(cells["model"].astype(str)) 254 if len(models) != 1: 255 raise DatasetError( 256 f"Ego tracking analysis requires one model, got {sorted(models)}." 257 ) 258 if cells.duplicated(["scene", "phase"]).any(): 259 raise DatasetError("Ego tracking cells contain duplicate scene-phase rows.") 260 split_counts = cells.groupby("split").size().to_dict() 261 if split_counts != EGO_SPLIT_CELLS or cells["scene"].nunique() != 100: 262 raise DatasetError( 263 "Ego analysis requires 100 scenes split 33/33/34; got " 264 f"{split_counts} across {cells['scene'].nunique()} scenes." 265 ) 266 267 metric_columns = {metric: f"metric:{metric}" for metric in PAPER_TRACKING_METRICS} 268 for metric, column in metric_columns.items(): 269 values = cells[column].astype(float).to_numpy() 270 if not np.isfinite(values).all(): 271 raise DatasetError(f"Non-finite ego tracking metric: {metric}.") 272 metric_means = { 273 "overall": { 274 metric: float(cells[column].astype(float).mean()) 275 for metric, column in metric_columns.items() 276 }, 277 "per_difficulty": { 278 split: { 279 metric: float(rows[column].astype(float).mean()) 280 for metric, column in metric_columns.items() 281 } 282 for split, rows in cells.groupby("split", sort=True) 283 }, 284 } 285 hardware_columns = ( 286 "latency_ms", 287 "throughput_fps", 288 "peak_gpu_memory_allocated_mb", 289 "peak_gpu_memory_reserved_mb", 290 "clip_wall_time_s", 291 "parameter_count", 292 ) 293 hardware_means = { 294 column: float(cells[column].astype(float).mean()) 295 for column in hardware_columns 296 if column in cells and np.isfinite(cells[column].astype(float)).all() 297 } 298 analysis = { 299 "schema_version": "rpx-ego-tracking-analysis-v1", 300 "dataset_protocol": "ego", 301 "model": next(iter(models)), 302 "metrics": list(PAPER_TRACKING_METRICS), 303 "directions": TRACKING_DIRECTIONS, 304 "n_scenes": 100, 305 "n_cells": len(cells), 306 "split_cells": split_counts, 307 "metric_means": metric_means, 308 "hardware_means": hardware_means, 309 } 310 return analysis, cells
Aggregate the one-clip-per-scene RPX ego tracking protocol.
def
write_ego_tracking_analysis( analysis: Mapping[str, Any], cells: Any, output_dir: str | pathlib.Path) -> dict[str, pathlib.Path]:
313def write_ego_tracking_analysis( 314 analysis: Mapping[str, Any], 315 cells: Any, 316 output_dir: str | Path, 317) -> dict[str, Path]: 318 import pandas as pd 319 320 output = Path(output_dir) 321 output.mkdir(parents=True, exist_ok=True) 322 paths = { 323 "combined_cells": output / "combined_cells.parquet", 324 "json": output / "paper_analysis.json", 325 "markdown": output / "paper_analysis.md", 326 "table": output / "paper_table.csv", 327 } 328 cells.to_parquet(paths["combined_cells"], index=False) 329 paths["json"].write_text( 330 json.dumps(dict(analysis), indent=2, sort_keys=True) + "\n", 331 encoding="utf-8", 332 ) 333 rows = [] 334 for split, values in ( 335 [("overall", analysis["metric_means"]["overall"])] 336 + list(analysis["metric_means"]["per_difficulty"].items()) 337 ): 338 rows.append({"model": analysis["model"], "split": split, **values}) 339 pd.DataFrame(rows).to_csv(paths["table"], index=False) 340 lines = [ 341 "# RPX ego tracking analysis", 342 "", 343 f"- Model: `{analysis['model']}`", 344 f"- Clips/scenes: {analysis['n_cells']}", 345 "- Protocol: one ego video per scene", 346 "", 347 "| Split | HOTA | DetA | AssA | IDF1 | MOTA | ID switches |", 348 "|---|---:|---:|---:|---:|---:|---:|", 349 ] 350 for row in rows: 351 lines.append( 352 f"| {row['split']} | {row['hota']:.6g} | {row['deta']:.6g} | " 353 f"{row['assa']:.6g} | {row['idf1']:.6g} | " 354 f"{row['mota']:.6g} | {row['idsw']:.6g} |" 355 ) 356 paths["markdown"].write_text("\n".join(lines) + "\n", encoding="utf-8") 357 return paths
def
write_tracking_analysis( analysis: Mapping[str, Any], cells: Any, output_dir: str | pathlib.Path) -> dict[str, pathlib.Path]:
360def write_tracking_analysis( 361 analysis: Mapping[str, Any], 362 cells: Any, 363 output_dir: str | Path, 364) -> dict[str, Path]: 365 import pandas as pd 366 367 output = Path(output_dir) 368 output.mkdir(parents=True, exist_ok=True) 369 paths = { 370 "combined_cells": output / "combined_cells.parquet", 371 "json": output / "paper_analysis.json", 372 "markdown": output / "paper_analysis.md", 373 "table": output / "paper_table.csv", 374 } 375 cells.to_parquet(paths["combined_cells"], index=False) 376 paths["json"].write_text( 377 json.dumps(dict(analysis), indent=2, sort_keys=True) + "\n", 378 encoding="utf-8", 379 ) 380 overall = analysis["overall_phase_manova"] 381 table = { 382 "model": analysis["model"], 383 "phi": overall["phi_wilks"], 384 "wilks_lambda": overall["wilks_lambda"], 385 "p_value": overall["p_value"], 386 "partial_eta_squared": overall["partial_eta_squared"], 387 "jedi_status": analysis["jedi_status"], 388 } 389 pd.DataFrame([table]).to_csv(paths["table"], index=False) 390 lines = [ 391 "# RPX D3 paper analysis", 392 "", 393 f"- Model: `{analysis['model']}`", 394 f"- Cells: {analysis['n_cells']} (100 scenes × 3 phases)", 395 f"- Φ: {overall['phi_wilks']:.6g}", 396 f"- Wilks’ Λ: {overall['wilks_lambda']:.6g}", 397 f"- Rao p: {overall['p_value']:.6g}", 398 f"- Partial η²: {overall['partial_eta_squared']:.6g}", 399 f"- JEDI: `{analysis['jedi_status']}`", 400 "", 401 "## Per-phase means", 402 "", 403 "| Phase | HOTA | DetA | AssA | IDF1 | MOTA | ID switches |", 404 "|---|---:|---:|---:|---:|---:|---:|", 405 ] 406 for phase in PHASES: 407 values = analysis["metric_means"]["per_phase"][phase] 408 lines.append( 409 f"| {phase} | {values['hota']:.6g} | {values['deta']:.6g} | " 410 f"{values['assa']:.6g} | {values['idf1']:.6g} | " 411 f"{values['mota']:.6g} | {values['idsw']:.6g} |" 412 ) 413 paths["markdown"].write_text("\n".join(lines) + "\n", encoding="utf-8") 414 return paths