Skip to content

Commit b06c6fe

Browse files
committed
add prediction param for care scores
1 parent f0e9699 commit b06c6fe

7 files changed

Lines changed: 315 additions & 36 deletions

File tree

scripts/analysis/compare_comprehensive.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ function printScenarioDetails(comp: any, name1: string, name2: string) {
9292

9393
// CARE Components
9494
const care = comp.care_components;
95+
console.log(`${'CARE Score (Composite)'.padEnd(25)} | ${care.care_score.toFixed(2).padEnd(25)} | ${care.comp_care_score.toFixed(2).padEnd(25)}`);
9596
console.log(`${'CARE Sentence Effort'.padEnd(25)} | ${care.sentences.toFixed(3).padEnd(25)} | ${care.comp_sentences.toFixed(3).padEnd(25)}`);
9697
console.log(`${'Core Vocabulary Found'.padEnd(25)} | ${care.core.toString().padEnd(25)} | ${care.comp_core.toString().padEnd(25)}`);
9798
console.log(`${'Fringe Vocabulary Found'.padEnd(25)} | ${care.fringe.toString().padEnd(25)} | ${care.comp_fringe.toString().padEnd(25)}`);

scripts/analysis/compare_pagesets.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,28 @@ import * as path from 'path';
77
async function main() {
88
const args = process.argv.slice(2);
99
if (args.length < 2) {
10-
console.log('Usage: npx ts-node compare_pagesets.ts <file1> <file2> [--spelling1 <id>] [--spelling2 <id>]');
10+
console.log('Usage: npx ts-node compare_pagesets.ts <file1> <file2> [--spelling1 <id>] [--spelling2 <id>] [--use-prediction] [--prediction-selections <n>]');
11+
console.log('');
12+
console.log('Options:');
13+
console.log(' --use-prediction Use prediction instead of common word efforts for missing words');
14+
console.log(' --prediction-selections Average number of prediction selections (default: 1.5)');
15+
console.log('');
16+
console.log('Note: By default, missing words use common word baseline efforts (matching Ruby aac-metrics)');
1117
process.exit(1);
1218
}
1319

1420
const file1 = args[0];
1521
const file2 = args[1];
1622
let spelling1: string | undefined;
1723
let spelling2: string | undefined;
24+
let usePrediction = false;
25+
let predictionSelections = 1.5;
1826

1927
for (let i = 0; i < args.length; i++) {
2028
if (args[i] === '--spelling1') spelling1 = args[i + 1];
2129
if (args[i] === '--spelling2') spelling2 = args[i + 1];
30+
if (args[i] === '--use-prediction') usePrediction = true;
31+
if (args[i] === '--prediction-selections') predictionSelections = parseFloat(args[i + 1]);
2232
}
2333

2434
const calculator = new Analytics.MetricsCalculator();
@@ -27,7 +37,8 @@ async function main() {
2737
console.log(`\n📚 AAC Pageset Comparison`);
2838
console.log(`=========================`);
2939
console.log(`Target 1: ${path.basename(file1)}`);
30-
console.log(`Target 2: ${path.basename(file2)}\n`);
40+
console.log(`Target 2: ${path.basename(file2)}`);
41+
console.log(`Missing word method: ${usePrediction ? 'Prediction' : 'Common word efforts'}${usePrediction ? ` (selections: ${predictionSelections})` : ''}\n`);
3142

3243
try {
3344
// Load and analyze 1
@@ -40,7 +51,11 @@ async function main() {
4051
const tree2 = p2.loadIntoTree(path.resolve(process.cwd(), file2));
4152
const metrics2 = calculator.analyze(tree2, { spellingPageId: spelling2 });
4253

43-
const comparison = comparer.compare(metrics1, metrics2, { includeSentences: true });
54+
const comparison = comparer.compare(metrics1, metrics2, {
55+
includeSentences: true,
56+
usePrediction,
57+
predictionSelections
58+
});
4459

4560
printDetailedComparison(comparison, path.basename(file1), path.basename(file2));
4661

@@ -97,6 +112,11 @@ function printDetailedComparison(comp: any, name1: string, name2: string) {
97112
console.log(`Avg Sentence Effort: ${care.sentences.toFixed(3)} (T1) vs ${care.comp_sentences.toFixed(3)} (T2)`);
98113
console.log(`Fringe Words Found: ${care.fringe} (T1) vs ${care.comp_fringe} (T2)`);
99114
console.log(line);
115+
console.log(`\n📈 CARE SCORE (Composite)`);
116+
console.log(line);
117+
console.log(`T1 Score: ${care.care_score.toFixed(2)} (higher is better)`);
118+
console.log(`T2 Score: ${care.comp_care_score.toFixed(2)} (higher is better)`);
119+
console.log(line);
100120
console.log(`\n`);
101121
}
102122

src/utilities/analytics/metrics/comparison.ts

Lines changed: 215 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { MetricsResult, ButtonMetrics, ComparisonResult } from './types';
99
import { SentenceAnalyzer } from './sentence';
1010
import { VocabularyAnalyzer } from './vocabulary';
1111
import { ReferenceLoader } from '../reference/index';
12+
import { spellingEffort, predictionEffort } from './effort';
13+
import { MetricsOptions } from './types';
1214

1315
export class ComparisonAnalyzer {
1416
private vocabAnalyzer: VocabularyAnalyzer;
@@ -37,7 +39,7 @@ export class ComparisonAnalyzer {
3739
options?: {
3840
includeSentences?: boolean;
3941
locale?: string;
40-
}
42+
} & Partial<MetricsOptions>
4143
): ComparisonResult {
4244
// Create base result from target
4345
const baseResult = { ...targetResult };
@@ -102,7 +104,8 @@ export class ComparisonAnalyzer {
102104
const careComponents = this.calculateCareComponents(
103105
targetResult,
104106
compareResult,
105-
overlappingWords
107+
overlappingWords,
108+
options
106109
);
107110

108111
// Analyze high/low effort words
@@ -269,8 +272,61 @@ export class ComparisonAnalyzer {
269272
private calculateCareComponents(
270273
targetResult: MetricsResult,
271274
compareResult: MetricsResult,
272-
_overlappingWords: string[]
275+
_overlappingWords: string[],
276+
options?: {
277+
includeSentences?: boolean;
278+
locale?: string;
279+
} & Partial<MetricsOptions>
273280
): ComparisonResult['care_components'] {
281+
// Load common words with baseline efforts (matching Ruby line 527-534)
282+
const commonWordsData = this.referenceLoader.loadCommonWords();
283+
const commonWords = new Map<string, number>();
284+
commonWordsData.words.forEach((word: string) => {
285+
commonWords.set(word.toLowerCase(), commonWordsData.efforts[word] || 0);
286+
});
287+
288+
// Determine prediction settings (default: use common words efforts, not prediction)
289+
const usePrediction = options?.usePrediction || false; // Default FALSE (use common words)
290+
const predictionSelections = options?.predictionSelections || 1.5;
291+
const debugMode = process.env.DEBUG_METRICS === 'true';
292+
293+
// Helper function to calculate fallback effort
294+
const getFallbackEffort = (
295+
word: string,
296+
hasPrediction: boolean,
297+
spellingBaseEffort?: number
298+
): number => {
299+
const wordLower = word.toLowerCase();
300+
301+
// Check common words efforts first (matching Ruby line 533)
302+
if (commonWords.has(wordLower)) {
303+
const effort = commonWords.get(wordLower);
304+
return effort !== undefined ? effort : spellingEffort(word, 10, 2.5);
305+
}
306+
307+
// If usePrediction is true and prediction is available, use prediction
308+
if (usePrediction && hasPrediction && spellingBaseEffort !== undefined) {
309+
return predictionEffort(spellingBaseEffort, 2.5, predictionSelections, 2);
310+
}
311+
312+
// Fallback to manual spelling (matching Ruby spelling_effort: 10 + word.length * 2.5)
313+
return spellingEffort(word, 10, 2.5);
314+
};
315+
316+
// Debug: Check settings
317+
const targetHasPrediction =
318+
targetResult.has_dynamic_prediction && targetResult.spelling_effort_base !== undefined;
319+
const _compareHasPrediction =
320+
compareResult.has_dynamic_prediction && compareResult.spelling_effort_base !== undefined;
321+
if (debugMode) {
322+
console.log(`\n🔍 DEBUG Fallback Effort Settings:`);
323+
console.log(` Common words loaded: ${commonWords.size}`);
324+
console.log(` usePrediction option: ${usePrediction}`);
325+
console.log(` Target has prediction capability: ${targetHasPrediction}`);
326+
console.log(
327+
` Target spelling_base: ${targetResult.spelling_effort_base?.toFixed(2) || 'undefined'}`
328+
);
329+
}
274330
// Create word maps with normalized keys
275331
const targetWords = new Map<string, ButtonMetrics>();
276332
targetResult.buttons.forEach((btn) => {
@@ -293,68 +349,200 @@ export class ComparisonAnalyzer {
293349
// Load reference data
294350
const coreLists = this.referenceLoader.loadCoreLists();
295351
const fringe = this.referenceLoader.loadFringe();
352+
const commonFringe = this.referenceLoader.loadCommonFringe();
296353
const sentences = this.referenceLoader.loadSentences();
297354

298-
// Calculate core coverage
355+
// Calculate core coverage and effort (matching Ruby lines 609-647)
299356
let coreCount = 0;
300357
let compCoreCount = 0;
358+
let targetCoreEffort = 0;
359+
let compCoreEffort = 0;
301360
const allCoreWords = new Set<string>();
302361
coreLists.forEach((list) => {
303362
list.words.forEach((word) => allCoreWords.add(word.toLowerCase()));
304363
});
305364

306365
allCoreWords.forEach((word) => {
307366
const key = this.normalize(word);
308-
if (targetWords.has(key)) coreCount++;
309-
if (compareWords.has(key)) compCoreCount++;
367+
const targetBtn = targetWords.get(key);
368+
const compareBtn = compareWords.get(key);
369+
370+
if (targetBtn) {
371+
coreCount++;
372+
targetCoreEffort += targetBtn.effort;
373+
} else {
374+
// Fallback to spelling or prediction effort
375+
targetCoreEffort += getFallbackEffort(
376+
word,
377+
targetResult.has_dynamic_prediction || false,
378+
targetResult.spelling_effort_base
379+
);
380+
}
381+
382+
if (compareBtn) {
383+
compCoreCount++;
384+
compCoreEffort += compareBtn.effort;
385+
} else {
386+
compCoreEffort += getFallbackEffort(
387+
word,
388+
compareResult.has_dynamic_prediction || false,
389+
compareResult.spelling_effort_base
390+
);
391+
}
310392
});
311393

312-
// Calculate sentence construction effort
313-
let sentenceEffort = 0;
314-
let compSentenceEffort = 0;
315-
let sentenceWordCount = 0;
394+
const avgCoreEffort = allCoreWords.size > 0 ? targetCoreEffort / allCoreWords.size : 0;
395+
const avgCompCoreEffort = allCoreWords.size > 0 ? compCoreEffort / allCoreWords.size : 0;
396+
397+
// Calculate core component scores (matching Ruby lines 644-647)
398+
const coreScore = avgCoreEffort * 5.0;
399+
const compCoreScore = avgCompCoreEffort * 5.0;
400+
401+
// Calculate sentence construction effort (matching Ruby lines 654-668)
402+
const sentenceEfforts: number[] = [];
403+
const compSentenceEfforts: number[] = [];
316404

317405
sentences.forEach((words) => {
406+
let targetSentenceEffort = 0;
407+
let compSentenceEffort = 0;
408+
318409
words.forEach((word) => {
319410
const key = this.normalize(word);
320411
const targetBtn = targetWords.get(key);
321412
const compareBtn = compareWords.get(key);
322413

323414
if (targetBtn) {
324-
sentenceEffort += targetBtn.effort;
415+
targetSentenceEffort += targetBtn.effort;
325416
} else {
326-
sentenceEffort += 10 + word.length * 2.5; // Spelling effort
417+
targetSentenceEffort += getFallbackEffort(
418+
word,
419+
targetResult.has_dynamic_prediction || false,
420+
targetResult.spelling_effort_base
421+
);
327422
}
328423

329424
if (compareBtn) {
330425
compSentenceEffort += compareBtn.effort;
331426
} else {
332-
compSentenceEffort += 10 + word.length * 2.5;
427+
compSentenceEffort += getFallbackEffort(
428+
word,
429+
compareResult.has_dynamic_prediction || false,
430+
compareResult.spelling_effort_base
431+
);
333432
}
334-
335-
sentenceWordCount++;
336433
});
434+
435+
// Average effort per sentence (matching Ruby line 657)
436+
sentenceEfforts.push(targetSentenceEffort / words.length);
437+
compSentenceEfforts.push(compSentenceEffort / words.length);
337438
});
338439

339-
const avgSentenceEffort = sentenceWordCount > 0 ? sentenceEffort / sentenceWordCount : 0;
440+
const avgSentenceEffort =
441+
sentenceEfforts.length > 0
442+
? sentenceEfforts.reduce((a, b) => a + b, 0) / sentenceEfforts.length
443+
: 0;
340444
const compAvgSentenceEffort =
341-
sentenceWordCount > 0 ? compSentenceEffort / sentenceWordCount : 0;
445+
compSentenceEfforts.length > 0
446+
? compSentenceEfforts.reduce((a, b) => a + b, 0) / compSentenceEfforts.length
447+
: 0;
448+
449+
// Sentence component scores (matching Ruby line 665-668)
450+
const sentenceScore = avgSentenceEffort * 3.0;
451+
const compSentenceScore = compAvgSentenceEffort * 3.0;
342452

343-
// Calculate fringe coverage
453+
// Calculate fringe effort (matching Ruby lines 670-687)
454+
const fringeEfforts: number[] = [];
455+
const compFringeEfforts: number[] = [];
344456
let fringeCount = 0;
345457
let compFringeCount = 0;
458+
459+
fringe.forEach((word: string) => {
460+
const key = this.normalize(word);
461+
const targetBtn = targetWords.get(key);
462+
const compareBtn = compareWords.get(key);
463+
464+
if (targetBtn) {
465+
fringeEfforts.push(targetBtn.effort);
466+
fringeCount++;
467+
} else {
468+
fringeEfforts.push(
469+
getFallbackEffort(
470+
word,
471+
targetResult.has_dynamic_prediction || false,
472+
targetResult.spelling_effort_base
473+
)
474+
);
475+
}
476+
477+
if (compareBtn) {
478+
compFringeEfforts.push(compareBtn.effort);
479+
compFringeCount++;
480+
} else {
481+
compFringeEfforts.push(
482+
getFallbackEffort(
483+
word,
484+
compareResult.has_dynamic_prediction || false,
485+
compareResult.spelling_effort_base
486+
)
487+
);
488+
}
489+
});
490+
491+
const avgFringeEffort =
492+
fringeEfforts.length > 0
493+
? fringeEfforts.reduce((a, b) => a + b, 0) / fringeEfforts.length
494+
: 0;
495+
const avgCompFringeEffort =
496+
compFringeEfforts.length > 0
497+
? compFringeEfforts.reduce((a, b) => a + b, 0) / compFringeEfforts.length
498+
: 0;
499+
500+
// Fringe component scores (matching Ruby line 684-687)
501+
const fringeScore = avgFringeEffort * 2.0;
502+
const compFringeScore = avgCompFringeEffort * 2.0;
503+
504+
// Calculate common fringe effort (matching Ruby lines 689-705)
505+
const commonFringeEfforts: number[] = [];
506+
const compCommonFringeEfforts: number[] = [];
346507
let commonFringeCount = 0;
347508

348-
fringe.forEach((word) => {
509+
commonFringe.forEach((word: string) => {
349510
const key = this.normalize(word);
350-
const inTarget = targetWords.has(key);
351-
const inCompare = compareWords.has(key);
511+
const targetBtn = targetWords.get(key);
512+
const compareBtn = compareWords.get(key);
352513

353-
if (inTarget) fringeCount++;
354-
if (inCompare) compFringeCount++;
355-
if (inTarget && inCompare) commonFringeCount++;
514+
if (targetBtn && compareBtn) {
515+
commonFringeEfforts.push(targetBtn.effort);
516+
compCommonFringeEfforts.push(compareBtn.effort);
517+
commonFringeCount++;
518+
}
356519
});
357520

521+
const avgCommonFringeEffort =
522+
commonFringeEfforts.length > 0
523+
? commonFringeEfforts.reduce((a, b) => a + b, 0) / commonFringeEfforts.length
524+
: 0;
525+
const avgCompCommonFringeEffort =
526+
compCommonFringeEfforts.length > 0
527+
? compCommonFringeEfforts.reduce((a, b) => a + b, 0) / compCommonFringeEfforts.length
528+
: 0;
529+
530+
// Common fringe component scores (matching Ruby line 702-705)
531+
const commonFringeScore = avgCommonFringeEffort * 1.0;
532+
const compCommonFringeScore = avgCompCommonFringeEffort * 1.0;
533+
534+
// Calculate total CARE effort tally (matching Ruby lines 707-708)
535+
const PLACEHOLDER = 70;
536+
const targetEffortTally =
537+
coreScore + sentenceScore + fringeScore + commonFringeScore + PLACEHOLDER;
538+
const compEffortTally =
539+
compCoreScore + compSentenceScore + compFringeScore + compCommonFringeScore + PLACEHOLDER;
540+
541+
// Calculate final CARE scores (matching Ruby line 710-711)
542+
// res[:target_effort_score] = [0.0, 350.0 - target_effort_tally].max
543+
const careScore = Math.max(0, 350.0 - targetEffortTally);
544+
const compCareScore = Math.max(0, 350.0 - compEffortTally);
545+
358546
return {
359547
core: coreCount,
360548
comp_core: compCoreCount,
@@ -364,6 +552,9 @@ export class ComparisonAnalyzer {
364552
comp_fringe: compFringeCount,
365553
common_fringe: commonFringeCount,
366554
comp_common_fringe: commonFringeCount,
555+
// New composite CARE scores
556+
care_score: careScore,
557+
comp_care_score: compCareScore,
367558
};
368559
}
369560

0 commit comments

Comments
 (0)