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
+12
-126
@@ -6,18 +6,12 @@
|
||||
import AnalysisDisplay from './components/AnalysisDisplay.svelte';
|
||||
import ExportControls from './components/ExportControls.svelte';
|
||||
import HelpModal from './components/HelpModal.svelte';
|
||||
import FormRecommendation from './components/FormRecommendation.svelte';
|
||||
import AnalysisTransitionBanner from './components/AnalysisTransitionBanner.svelte';
|
||||
import HamburgerMenu from './components/HamburgerMenu.svelte';
|
||||
import Landing from './components/Landing.svelte';
|
||||
import { BicorderClassifier } from './bicorder-classifier';
|
||||
|
||||
// Load bicorder data and model from build-time constants
|
||||
let data: BicorderState = JSON.parse(JSON.stringify(__BICORDER_DATA__));
|
||||
const model = __BICORDER_MODEL__;
|
||||
|
||||
// Initialize classifier
|
||||
const classifier = new BicorderClassifier(model, data.version);
|
||||
|
||||
// Initialize timestamp if not set
|
||||
if (!data.metadata.timestamp) {
|
||||
@@ -87,10 +81,12 @@
|
||||
});
|
||||
|
||||
// Analysis screens (shown in both shortform and longform)
|
||||
// Show the useful gradient first (index 3), then the others
|
||||
const analysisOrder = [3, 0, 1, 2]; // useful, hardness, polarization, formal/informal
|
||||
// Show the useful gradient first, then the automated ones
|
||||
const analysisOrder = [2, 0, 1]; // useful, hardness, polarization
|
||||
analysisOrder.forEach((index) => {
|
||||
screens.push({ type: 'analysis', index, gradient: data.analysis[index] });
|
||||
if (index < data.analysis.length) {
|
||||
screens.push({ type: 'analysis', index, gradient: data.analysis[index] });
|
||||
}
|
||||
});
|
||||
|
||||
// Export screen
|
||||
@@ -180,57 +176,6 @@
|
||||
.flatMap(set => set.gradients)
|
||||
.filter(g => !data.metadata.shortform || g.shortform).length;
|
||||
|
||||
// Calculate form recommendation (shared by FormRecommendation and AnalysisTransitionBanner)
|
||||
let formRecommendation: any = null;
|
||||
let hasEnoughDataForRecommendation = false;
|
||||
|
||||
$: {
|
||||
// Collect ratings from diagnostic data
|
||||
const ratings: Record<string, number> = {};
|
||||
let valueCount = 0;
|
||||
let shortFormTotal = 0;
|
||||
|
||||
data.diagnostic.forEach((diagnosticSet) => {
|
||||
const setName = diagnosticSet.set_name;
|
||||
diagnosticSet.gradients.forEach((gradient) => {
|
||||
// Count shortform gradients
|
||||
if (gradient.shortform) {
|
||||
shortFormTotal++;
|
||||
}
|
||||
|
||||
if (gradient.value !== null) {
|
||||
const dimensionName = `${setName}_${gradient.term_left}_vs_${gradient.term_right}`;
|
||||
ratings[dimensionName] = gradient.value;
|
||||
|
||||
// Only count shortform values for the threshold
|
||||
if (gradient.shortform) {
|
||||
valueCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Only calculate if at least half of shortform gradients are complete
|
||||
const threshold = Math.ceil(shortFormTotal / 2);
|
||||
hasEnoughDataForRecommendation = valueCount >= threshold;
|
||||
|
||||
if (hasEnoughDataForRecommendation && data.metadata.shortform) {
|
||||
try {
|
||||
const prediction = classifier.predict(ratings, { detailed: true });
|
||||
const assessment = classifier.assessShortFormReadiness(ratings);
|
||||
formRecommendation = {
|
||||
...prediction,
|
||||
...assessment,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error calculating form recommendation:', error);
|
||||
formRecommendation = null;
|
||||
}
|
||||
} else {
|
||||
formRecommendation = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Load saved state from localStorage
|
||||
onMount(() => {
|
||||
const saved = localStorage.getItem('bicorder-state');
|
||||
@@ -328,79 +273,21 @@
|
||||
return Math.round(Math.max(1, Math.min(9, polarizationScore)));
|
||||
}
|
||||
|
||||
function ldaScoreToScale(ldaScore: number | null): number | null {
|
||||
/**
|
||||
* Convert LDA score to the analysis[2] "formal vs informal" 1-9 scale.
|
||||
* LDA scores typically range from -4 to +4 (8 range); target is 1-9.
|
||||
*
|
||||
* The model's sign convention (see analysis/bicorder_model.json):
|
||||
* positive LDA → cluster 2 = Institutional/Bureaucratic = "formal"
|
||||
* negative LDA → cluster 1 = Relational/Cultural = "informal"
|
||||
* bicorder.json defines this gradient as 1 = formal, 9 = informal, so a
|
||||
* positive LDA score must map toward 1 (formal). The score is therefore
|
||||
* subtracted, not added.
|
||||
*
|
||||
* Formula: value = 5 - (ldaScore * 4/3)
|
||||
* - LDA +3 or more → 1 (formal / institutional / bureaucratic)
|
||||
* - LDA 0 → 5 (boundary, characteristics of both families)
|
||||
* - LDA -3 or less → 9 (informal / relational / cultural)
|
||||
*/
|
||||
if (ldaScore === null) return null;
|
||||
|
||||
const value = 5 - (ldaScore * 4.0 / 3.0);
|
||||
|
||||
// Clamp to 1-9 range and round
|
||||
return Math.round(Math.max(1, Math.min(9, value)));
|
||||
}
|
||||
|
||||
function calculateFormalInformal(): number | null {
|
||||
// Collect all diagnostic gradients with their set and gradient info
|
||||
const ratings: Record<string, number> = {};
|
||||
|
||||
data.diagnostic.forEach((diagnosticSet) => {
|
||||
const setName = diagnosticSet.set_name;
|
||||
diagnosticSet.gradients.forEach((gradient) => {
|
||||
if (gradient.value !== null) {
|
||||
// Dimension name must match the model's keys: SetName_left_vs_right
|
||||
const dimensionName = `${setName}_${gradient.term_left}_vs_${gradient.term_right}`;
|
||||
ratings[dimensionName] = gradient.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Check if we have any ratings
|
||||
if (Object.keys(ratings).length === 0) return null;
|
||||
|
||||
try {
|
||||
// Get prediction from classifier (need detailed: true to get ldaScore)
|
||||
const result = classifier.predict(ratings, { detailed: true });
|
||||
|
||||
// Convert LDA score to the 1-9 formal/informal scale
|
||||
return ldaScoreToScale(result.ldaScore);
|
||||
} catch (error) {
|
||||
console.error('Error calculating formal/informal score:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Update automated analysis values reactively
|
||||
// Note: the formal/informal (LDA classifier) analysis was removed from the
|
||||
// bicorder in v1.3.0. Cluster classification lives on as research in
|
||||
// analysis/ (see scripts/bicorder_classifier.py).
|
||||
$: {
|
||||
data.analysis.forEach((item, index) => {
|
||||
if (item.automated) {
|
||||
if (index === 0) {
|
||||
// Hardness/Softness
|
||||
data.analysis[0].value = calculateHardness();
|
||||
} else if (index === 1) {
|
||||
// Polarized/Centrist
|
||||
data.analysis[1].value = calculatePolarization();
|
||||
} else if (index === 2) {
|
||||
// Formal/Informal (LDA classifier)
|
||||
data.analysis[2].value = calculateFormalInformal();
|
||||
if (item.term_left === 'hardness') {
|
||||
item.value = calculateHardness();
|
||||
} else if (item.term_left === 'polarized') {
|
||||
item.value = calculatePolarization();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleMetadataUpdate(event: CustomEvent) {
|
||||
// Properly trigger reactivity for nested metadata changes
|
||||
data = {
|
||||
@@ -604,7 +491,6 @@
|
||||
|
||||
{#if isFirstAnalysisScreen}
|
||||
<AnalysisTransitionBanner
|
||||
recommendation={formRecommendation}
|
||||
isShortForm={data.metadata.shortform}
|
||||
completedGradients={completedGradientsCount}
|
||||
allAnalysisGradients={data.analysis}
|
||||
|
||||
Reference in new issue
Block a user