|
|
@@ -8,12 +8,13 @@ use Illuminate\Support\Facades\Schema;
|
|
|
class QuestionDifficultyResolver
|
|
|
{
|
|
|
private const TABLE = 'question_difficulty_calibrations';
|
|
|
+ private const SERVED_DIFF_CONFIDENCE_DENOMINATOR = 20.0;
|
|
|
|
|
|
private ?bool $tableReady = null;
|
|
|
|
|
|
/**
|
|
|
* @param array<int, int|string> $questionIds
|
|
|
- * @return array<int, float> question_bank_id => calibrated_difficulty
|
|
|
+ * @return array<int, array<string, float|null>> question_bank_id => calibration snapshot
|
|
|
*/
|
|
|
public function mapCalibratedDifficulty(array $questionIds): array
|
|
|
{
|
|
|
@@ -33,13 +34,29 @@ class QuestionDifficultyResolver
|
|
|
|
|
|
return DB::table(self::TABLE)
|
|
|
->whereIn('question_bank_id', $questionIds)
|
|
|
- ->pluck('calibrated_difficulty', 'question_bank_id')
|
|
|
- ->map(fn ($v) => (float) $v)
|
|
|
+ ->get(['question_bank_id', 'original_difficulty', 'calibrated_difficulty', 'weighted_attempts'])
|
|
|
+ ->mapWithKeys(function ($row) {
|
|
|
+ $qid = (int) ($row->question_bank_id ?? 0);
|
|
|
+ if ($qid <= 0) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ $qid => [
|
|
|
+ 'original_difficulty' => $row->original_difficulty !== null ? (float) $row->original_difficulty : null,
|
|
|
+ 'calibrated_difficulty' => $row->calibrated_difficulty !== null ? (float) $row->calibrated_difficulty : null,
|
|
|
+ 'weighted_attempts' => $row->weighted_attempts !== null ? (float) $row->weighted_attempts : null,
|
|
|
+ ],
|
|
|
+ ];
|
|
|
+ })
|
|
|
->all();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 批量给题目数组覆盖 difficulty(校准值优先,原始值兜底)
|
|
|
+ * 批量给题目数组计算组卷使用难度 served_diff:
|
|
|
+ * served_diff = alpha * calibrated + (1 - alpha) * original
|
|
|
+ * alpha = min(1, weighted_attempts / 20)
|
|
|
+ * 前提:仅当 calibrated_difficulty 存在时才融合;否则保持原始 difficulty 不变。
|
|
|
*
|
|
|
* @param array<int, array<string, mixed>> $questions
|
|
|
* @return array<int, array<string, mixed>>
|
|
|
@@ -64,10 +81,29 @@ class QuestionDifficultyResolver
|
|
|
|
|
|
foreach ($questions as &$q) {
|
|
|
$id = (int) ($q['id'] ?? $q['question_id'] ?? $q['question_bank_id'] ?? 0);
|
|
|
- if ($id > 0 && array_key_exists($id, $map)) {
|
|
|
- $q['difficulty'] = (float) $map[$id];
|
|
|
- $q['difficulty_source'] = 'calibrated';
|
|
|
+ if ($id <= 0 || ! array_key_exists($id, $map)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $snapshot = $map[$id] ?? [];
|
|
|
+ $calibrated = $snapshot['calibrated_difficulty'] ?? null;
|
|
|
+ if ($calibrated === null) {
|
|
|
+ continue;
|
|
|
}
|
|
|
+
|
|
|
+ $original = isset($q['difficulty']) && is_numeric($q['difficulty'])
|
|
|
+ ? (float) $q['difficulty']
|
|
|
+ : (float) ($snapshot['original_difficulty'] ?? $calibrated);
|
|
|
+ $weightedAttempts = max(0.0, (float) ($snapshot['weighted_attempts'] ?? 0.0));
|
|
|
+ $alpha = min(1.0, $weightedAttempts / self::SERVED_DIFF_CONFIDENCE_DENOMINATOR);
|
|
|
+ $servedDifficulty = ($alpha * (float) $calibrated) + ((1.0 - $alpha) * $original);
|
|
|
+
|
|
|
+ $q['difficulty'] = round($servedDifficulty, 4);
|
|
|
+ $q['difficulty_source'] = $alpha >= 0.9999 ? 'calibrated' : 'served_blend';
|
|
|
+ $q['difficulty_original'] = round($original, 4);
|
|
|
+ $q['difficulty_calibrated'] = round((float) $calibrated, 4);
|
|
|
+ $q['difficulty_alpha'] = round($alpha, 4);
|
|
|
+ $q['difficulty_weighted_attempts'] = round($weightedAttempts, 4);
|
|
|
}
|
|
|
unset($q);
|
|
|
|
|
|
@@ -85,4 +121,3 @@ class QuestionDifficultyResolver
|
|
|
return $this->tableReady;
|
|
|
}
|
|
|
}
|
|
|
-
|