feat: remove formal/informal LDA analysis from the bicorder (v1.3.0)
The two-family cluster classification is a research finding, not a
diagnostic; embedding it in the tool caused recurring bugs:
- ascii_bicorder.py had an inverted LDA sign (institutional mapped to 9,
not 1) and a stale term check ('bureaucratic') that silently disabled
the calculation after the Feb 2026 rename — output was always null
- The web app and Python script had divergent sign conventions and
divergent version-mismatch behavior (skip vs. continue with stale model)
Changes:
- bicorder.json: drop the formal/informal analysis gradient; version 1.3.0
- ascii_bicorder.py: remove all LDA/model machinery; keep hardness and
polarization as the only automated analyses
- App.svelte: remove classifier import, model constant, LDA calculation,
and form-recommendation reactive block; dispatch automated analyses by
term_left instead of array index
- Delete bicorder-classifier.ts and FormRecommendation.svelte (the latter
was imported but never rendered)
- AnalysisTransitionBanner: remove recommendation alert and prop; fix
hardcoded index checks that referenced the removed gradient
- vite.config.ts / vite-env.d.ts: stop loading bicorder_model.json
The cluster classifier lives on as research in analysis/ (scripts and
model untouched there). bicorder.txt regenerated.
This commit is contained in:
1 parent
fb3bebcea0
commit
8c40ca076b
9 files changed
+67
-1040
No files matched your search
+4
-106
@@ -6,93 +6,9 @@ Generate bicorder.txt from bicorder.json
|
||||
import json
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# Simple version-based approach
|
||||
#
|
||||
# The model includes a 'bicorder_version' field indicating which version of
|
||||
# bicorder.json it was trained on. The code checks that versions match before
|
||||
# calculating. This ensures the gradient structure is compatible.
|
||||
#
|
||||
# When bicorder.json changes (gradients added/removed/reordered), update the
|
||||
# version number and retrain the model.
|
||||
|
||||
|
||||
def load_classifier_model():
|
||||
"""Load the LDA model from bicorder_model.json"""
|
||||
# Try to find the model file
|
||||
script_dir = Path(__file__).parent
|
||||
model_paths = [
|
||||
script_dir / 'analysis' / 'bicorder_model.json',
|
||||
script_dir / 'bicorder_model.json',
|
||||
Path('analysis/bicorder_model.json'),
|
||||
Path('bicorder_model.json'),
|
||||
]
|
||||
|
||||
for path in model_paths:
|
||||
if path.exists():
|
||||
with open(path, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def calculate_lda_score(values_array, model):
|
||||
"""
|
||||
Calculate LDA score from an array of values using the model.
|
||||
|
||||
Args:
|
||||
values_array: list of 23 values (1-9) in the order expected by the model
|
||||
model: loaded classifier model
|
||||
|
||||
Returns:
|
||||
LDA score (float), or None if insufficient data
|
||||
"""
|
||||
if model is None:
|
||||
return None
|
||||
|
||||
if len(values_array) != len(model['dimensions']):
|
||||
return None
|
||||
|
||||
# Standardize using model scaler
|
||||
mean = model['scaler']['mean']
|
||||
scale = model['scaler']['scale']
|
||||
scaled = [(values_array[i] - mean[i]) / scale[i] for i in range(len(values_array))]
|
||||
|
||||
# Calculate LDA score: coef · x + intercept
|
||||
coef = model['lda']['coefficients']
|
||||
intercept = model['lda']['intercept']
|
||||
|
||||
# Dot product
|
||||
lda_score = sum(coef[i] * scaled[i] for i in range(len(scaled))) + intercept
|
||||
|
||||
return lda_score
|
||||
|
||||
|
||||
def lda_score_to_scale(lda_score):
|
||||
"""
|
||||
Convert LDA score to 1-9 scale.
|
||||
LDA scores typically range from -4 to +4 (8 range)
|
||||
Target scale is 1 to 9 (8 range)
|
||||
|
||||
Formula: value = 5 + (lda_score * 4/3)
|
||||
- LDA -3 or less → 1 (bureaucratic)
|
||||
- LDA 0 → 5 (boundary)
|
||||
- LDA +3 or more → 9 (relational)
|
||||
"""
|
||||
if lda_score is None:
|
||||
return None
|
||||
|
||||
# Scale: value = 5 + (lda_score * 1.33)
|
||||
value = 5 + (lda_score * 4.0 / 3.0)
|
||||
|
||||
# Clamp to 1-9 range and round
|
||||
value = max(1, min(9, value))
|
||||
return round(value)
|
||||
|
||||
|
||||
def calculate_hardness(diagnostic_values):
|
||||
"""Calculate hardness/softness (mean of all diagnostic values)"""
|
||||
if not diagnostic_values:
|
||||
@@ -133,34 +49,24 @@ def calculate_automated_analysis(json_data):
|
||||
"""
|
||||
Calculate values for automated analysis fields.
|
||||
Modifies json_data in place.
|
||||
|
||||
Note: the formal/informal (LDA classifier) analysis was removed from the
|
||||
bicorder in v1.3.0. The cluster classification lives on as research in
|
||||
analysis/ (see scripts/bicorder_classifier.py).
|
||||
"""
|
||||
# Collect all diagnostic values in order
|
||||
diagnostic_values = []
|
||||
values_array = []
|
||||
|
||||
for diagnostic_set in json_data.get("diagnostic", []):
|
||||
for gradient in diagnostic_set.get("gradients", []):
|
||||
value = gradient.get("value")
|
||||
if value is not None:
|
||||
diagnostic_values.append(value)
|
||||
values_array.append(float(value))
|
||||
else:
|
||||
# Fill missing with neutral value
|
||||
values_array.append(5.0)
|
||||
|
||||
# Only calculate if we have diagnostic values
|
||||
if not diagnostic_values:
|
||||
return
|
||||
|
||||
# Load classifier model
|
||||
model = load_classifier_model()
|
||||
|
||||
# Check version compatibility
|
||||
bicorder_version = json_data.get("version", "unknown")
|
||||
model_version = model.get("bicorder_version", "unknown") if model else "unknown"
|
||||
|
||||
version_mismatch = (model and bicorder_version != model_version)
|
||||
|
||||
# Calculate each automated analysis field
|
||||
for analysis_item in json_data.get("analysis", []):
|
||||
if not analysis_item.get("automated", False):
|
||||
@@ -173,14 +79,6 @@ def calculate_automated_analysis(json_data):
|
||||
analysis_item["value"] = calculate_hardness(diagnostic_values)
|
||||
elif term_left == "polarized":
|
||||
analysis_item["value"] = calculate_polarization(diagnostic_values)
|
||||
elif term_left == "bureaucratic":
|
||||
if version_mismatch:
|
||||
# Skip calculation if versions don't match
|
||||
print(f"Warning: Model version ({model_version}) doesn't match bicorder version ({bicorder_version}). Skipping bureaucratic/relational calculation.")
|
||||
analysis_item["value"] = None
|
||||
elif model:
|
||||
lda_score = calculate_lda_score(values_array, model)
|
||||
analysis_item["value"] = lda_score_to_scale(lda_score)
|
||||
|
||||
|
||||
def center_text(text, width):
|
||||
|
||||
Reference in new issue
Block a user