feat: add motion-loading helpers to axis_classifier
This commit is contained in:
@@ -11,6 +11,7 @@ from typing import Dict, List, Optional, Tuple
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import re
|
import re
|
||||||
|
import json
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -145,6 +146,102 @@ def _classify_from_titles(titles: List[str]) -> Tuple[Optional[str], float]:
|
|||||||
return best_cats[0], confidence
|
return best_cats[0], confidence
|
||||||
|
|
||||||
|
|
||||||
|
def _load_motion_vectors(db_path: str, window_id: str) -> Dict[int, np.ndarray]:
|
||||||
|
"""Load SVD motion vectors for a given window from DuckDB.
|
||||||
|
|
||||||
|
Returns {motion_id: vector_array}. Returns {} on any error.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import duckdb
|
||||||
|
|
||||||
|
conn = duckdb.connect(db_path, read_only=True)
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT entity_id, vector FROM svd_vectors "
|
||||||
|
"WHERE entity_type = 'motion' AND window_id = ?",
|
||||||
|
[window_id],
|
||||||
|
).fetchall()
|
||||||
|
conn.close()
|
||||||
|
result: Dict[int, np.ndarray] = {}
|
||||||
|
for entity_id, vector_raw in rows:
|
||||||
|
try:
|
||||||
|
mid = int(entity_id)
|
||||||
|
vec = np.array(json.loads(vector_raw), dtype=float)
|
||||||
|
result[mid] = vec
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return result
|
||||||
|
except Exception as exc:
|
||||||
|
_logger.debug("Failed to load motion vectors for window %s: %s", window_id, exc)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _project_motions(
|
||||||
|
motion_vecs: Dict[int, np.ndarray],
|
||||||
|
x_axis: np.ndarray,
|
||||||
|
y_axis: np.ndarray,
|
||||||
|
global_mean: np.ndarray,
|
||||||
|
) -> Dict[int, Tuple[float, float]]:
|
||||||
|
"""Project motion vectors onto the PCA axes after centering by global_mean.
|
||||||
|
|
||||||
|
Returns {motion_id: (x_score, y_score)}.
|
||||||
|
"""
|
||||||
|
projections: Dict[int, Tuple[float, float]] = {}
|
||||||
|
for mid, vec in motion_vecs.items():
|
||||||
|
try:
|
||||||
|
centered = vec - global_mean
|
||||||
|
x_score = float(np.dot(centered, x_axis))
|
||||||
|
y_score = float(np.dot(centered, y_axis))
|
||||||
|
projections[mid] = (x_score, y_score)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return projections
|
||||||
|
|
||||||
|
|
||||||
|
def _top_motion_ids(
|
||||||
|
projections: Dict[int, Tuple[float, float]],
|
||||||
|
axis: str,
|
||||||
|
n: int = 5,
|
||||||
|
) -> Dict[str, List[int]]:
|
||||||
|
"""Return the top-n motion IDs at each pole of the given axis.
|
||||||
|
|
||||||
|
axis: 'x' or 'y'
|
||||||
|
Returns {'+': [motion_ids], '-': [motion_ids]} (highest positive first,
|
||||||
|
most negative first in the '-' list).
|
||||||
|
"""
|
||||||
|
idx = 0 if axis == "x" else 1
|
||||||
|
sorted_ids = sorted(projections, key=lambda mid: projections[mid][idx])
|
||||||
|
neg_ids = sorted_ids[:n] # most negative
|
||||||
|
pos_ids = sorted_ids[-n:][::-1] # most positive
|
||||||
|
return {"+": pos_ids, "-": neg_ids}
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_motion_titles(
|
||||||
|
db_path: str,
|
||||||
|
motion_ids: List[int],
|
||||||
|
) -> Dict[int, Tuple[str, str]]:
|
||||||
|
"""Fetch (title, date) for a list of motion IDs from DuckDB.
|
||||||
|
|
||||||
|
Returns {motion_id: (title, date_str)}. Missing IDs are omitted.
|
||||||
|
Returns {} on any DB error.
|
||||||
|
"""
|
||||||
|
if not motion_ids:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
import duckdb
|
||||||
|
|
||||||
|
placeholders = ", ".join("?" * len(motion_ids))
|
||||||
|
conn = duckdb.connect(db_path, read_only=True)
|
||||||
|
rows = conn.execute(
|
||||||
|
f"SELECT id, title, date FROM motions WHERE id IN ({placeholders})",
|
||||||
|
motion_ids,
|
||||||
|
).fetchall()
|
||||||
|
conn.close()
|
||||||
|
return {int(row[0]): (str(row[1]), str(row[2])) for row in rows}
|
||||||
|
except Exception as exc:
|
||||||
|
_logger.debug("Failed to fetch motion titles: %s", exc)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def _load_ideology(csv_path: Path) -> Dict[str, Dict[str, float]]:
|
def _load_ideology(csv_path: Path) -> Dict[str, Dict[str, float]]:
|
||||||
"""Load party ideology scores from CSV.
|
"""Load party ideology scores from CSV.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user