| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- <?php
- namespace App\Services;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Log;
- use Illuminate\Support\Collection;
- /**
- * 知识掌握度计算引擎(PHP版本)
- * 基于BKT(Bayesian Knowledge Tracing)模型和多种因素综合计算
- *
- * 参考LearningAnalytics的Python实现迁移而来
- */
- class MasteryCalculator
- {
- /**
- * 难度权重配置
- * 简单题目权重低,困难题目权重高
- */
- private const DIFFICULTY_WEIGHTS = [
- 0.30 => 0.8, // 简单题目权重
- 0.60 => 1.0, // 中等题目权重
- 0.85 => 1.3, // 困难题目权重
- ];
- /**
- * 时间效率基准值(秒)
- * 不同难度题目的平均完成时间
- */
- private const TIME_BASELINE = [
- 0.30 => 60, // 简单题平均用时
- 0.60 => 120, // 中等题平均用时
- 0.85 => 180, // 困难题平均用时
- ];
- /**
- * 掌握度阈值配置
- */
- private const MASTERY_THRESHOLD_WEAK = 0.50; // 薄弱点阈值
- private const MASTERY_THRESHOLD_GOOD = 0.70; // 良好阈值
- private const MASTERY_THRESHOLD_MASTER = 0.85; // 掌握阈值
- /**
- * 最小练习题目数量
- */
- private const MIN_PRACTICE_QUESTIONS = 5;
- /**
- * 最小正确率要求
- */
- private const MIN_CORRECT_RATE = 0.60;
- /**
- * 计算学生对指定知识点的掌握度
- *
- * @param string $studentId 学生ID
- * @param string $kpCode 知识点编码
- * @param array|null $attempts 答题记录(可选,默认从数据库查询)
- * @return array 返回['mastery' => 掌握度, 'confidence' => 置信度, 'trend' => 趋势]
- */
- public function calculateMasteryLevel(string $studentId, string $kpCode, ?array $attempts = null): array
- {
- // 如果没有提供答题记录,从数据库查询
- if ($attempts === null) {
- $attempts = $this->getStudentAttempts($studentId, $kpCode);
- }
- if (empty($attempts)) {
- return [
- 'mastery' => 0.0,
- 'confidence' => 0.0,
- 'trend' => 'insufficient',
- 'total_attempts' => 0,
- 'correct_attempts' => 0,
- 'accuracy_rate' => 0.0,
- ];
- }
- // 1. 计算基础正确率
- $accuracyRate = $this->calculateAccuracyRate($attempts);
- // 2. 计算难度加权分数
- $difficultyScore = $this->calculateDifficultyScore($attempts);
- // 3. 计算时间效率分数
- $timeScore = $this->calculateTimeEfficiency($attempts);
- // 4. 计算技能熟练度影响
- $skillFactor = $this->calculateSkillFactor($attempts);
- // 5. 应用遗忘曲线衰减
- $decayFactor = $this->calculateDecayFactor($attempts);
- // 6. 综合计算掌握度
- $baseMastery = $accuracyRate *
- $difficultyScore *
- $timeScore *
- $skillFactor *
- $decayFactor;
- // 7. 计算置信度
- $confidence = $this->calculateConfidence($attempts);
- // 8. 判断趋势
- $trend = $this->determineTrend($attempts);
- // 9. 计算统计信息
- $totalAttempts = count($attempts);
- $correctAttempts = count(array_filter($attempts, fn($a) => $a['is_correct']));
- Log::info('MasteryCalculator::calculateMasteryLevel', [
- 'student_id' => $studentId,
- 'kp_code' => $kpCode,
- 'total_attempts' => $totalAttempts,
- 'correct_attempts' => $correctAttempts,
- 'accuracy_rate' => $accuracyRate,
- 'difficulty_score' => $difficultyScore,
- 'time_score' => $timeScore,
- 'skill_factor' => $skillFactor,
- 'decay_factor' => $decayFactor,
- 'final_mastery' => $baseMastery,
- 'confidence' => $confidence,
- 'trend' => $trend,
- ]);
- return [
- 'mastery' => round($baseMastery, 4),
- 'confidence' => round($confidence, 4),
- 'trend' => $trend,
- 'total_attempts' => $totalAttempts,
- 'correct_attempts' => $correctAttempts,
- 'accuracy_rate' => round($accuracyRate * 100, 2),
- 'details' => [
- 'accuracy_rate' => $accuracyRate,
- 'difficulty_score' => $difficultyScore,
- 'time_score' => $timeScore,
- 'skill_factor' => $skillFactor,
- 'decay_factor' => $decayFactor,
- ],
- ];
- }
- /**
- * 计算正确率
- */
- private function calculateAccuracyRate(array $attempts): float
- {
- if (empty($attempts)) {
- return 0.0;
- }
- $totalAttempts = count($attempts);
- $correctAttempts = count(array_filter($attempts, fn($a) => $a['is_correct']));
- $partialAttempts = count(array_filter($attempts, function($a) {
- return isset($a['partial_score']) && floatval($a['partial_score']) > 0;
- }));
- // 部分正确按50%计算
- $correctScore = $correctAttempts + $partialAttempts * 0.5;
- $accuracy = $correctScore / $totalAttempts;
- return min($accuracy, 1.0);
- }
- /**
- * 计算难度加权分数
- */
- private function calculateDifficultyScore(array $attempts): float
- {
- if (empty($attempts)) {
- return 0.0;
- }
- $weightedSum = 0.0;
- $totalWeight = 0.0;
- foreach ($attempts as $attempt) {
- $difficulty = floatval($attempt['question_difficulty'] ?? 0.6);
- $weight = self::DIFFICULTY_WEIGHTS[$difficulty] ?? 1.0;
- $score = $attempt['is_correct'] ? 1.0 : 0.0;
- $weightedSum += $score * $weight;
- $totalWeight += $weight;
- }
- if ($totalWeight == 0) {
- return 0.0;
- }
- return $weightedSum / $totalWeight;
- }
- /**
- * 计算时间效率分数
- */
- private function calculateTimeEfficiency(array $attempts): float
- {
- if (empty($attempts)) {
- return 0.0;
- }
- $efficiencyScores = [];
- foreach ($attempts as $attempt) {
- $difficulty = floatval($attempt['question_difficulty'] ?? 0.6);
- $baseline = self::TIME_BASELINE[$difficulty] ?? 120;
- $actualTime = floatval($attempt['attempt_time_seconds'] ?? 120);
- // 时间效率:基准时间/实际用时,最大不超过1.5
- $efficiency = min($baseline / max($actualTime, 1), 1.5);
- $efficiencyScores[] = $efficiency;
- }
- // 取最近5次答题的平均效率
- $recentEfficiency = array_slice($efficiencyScores, -5);
- $avgEfficiency = array_sum($recentEfficiency) / count($recentEfficiency);
- return min($avgEfficiency, 1.0);
- }
- /**
- * 计算技能熟练度影响
- */
- private function calculateSkillFactor(array $attempts, ?array $skillProficiency = null): float
- {
- // 简化实现:如果有技能熟练度数据,使用它;否则返回1.0
- if ($skillProficiency && !empty($skillProficiency)) {
- // 计算平均技能熟练度
- $avgProficiency = array_sum($skillProficiency) / count($skillProficiency);
- // 技能熟练度加权:范围0.8-1.2
- return 0.8 + ($avgProficiency * 0.4);
- }
- return 1.0;
- }
- /**
- * 计算遗忘曲线衰减因子
- */
- private function calculateDecayFactor(array $attempts): float
- {
- if (empty($attempts)) {
- return 0.0;
- }
- // 获取最近一次答题时间
- $lastAttemptTime = null;
- foreach ($attempts as $attempt) {
- $attemptTime = strtotime($attempt['completed_at'] ?? $attempt['created_at'] ?? 'now');
- if ($lastAttemptTime === null || $attemptTime > $lastAttemptTime) {
- $lastAttemptTime = $attemptTime;
- }
- }
- if ($lastAttemptTime === null) {
- return 1.0;
- }
- // 计算距离现在的天数
- $daysSinceLastAttempt = (time() - $lastAttemptTime) / 86400; // 86400 = 24*60*60
- // 遗忘曲线:每天衰减2%,最大衰减50%
- $decayRate = min($daysSinceLastAttempt * 0.02, 0.5);
- $decayFactor = 1.0 - $decayRate;
- return max($decayFactor, 0.5); // 最低保持50%
- }
- /**
- * 计算置信度
- */
- private function calculateConfidence(array $attempts): float
- {
- if (empty($attempts)) {
- return 0.0;
- }
- $totalAttempts = count($attempts);
- // 基于答题次数的置信度:答题越多,置信度越高
- // 5次以下线性增长,5次以上增长放缓
- if ($totalAttempts < 5) {
- $baseConfidence = $totalAttempts / 5.0;
- } else {
- $baseConfidence = 0.5 + (1.0 - 0.5) * (1 - exp(-($totalAttempts - 5) / 10));
- }
- // 正确率也影响置信度
- $accuracyRate = $this->calculateAccuracyRate($attempts);
- $accuracyFactor = 0.5 + $accuracyRate * 0.5;
- // 综合置信度
- $confidence = $baseConfidence * $accuracyFactor;
- return min($confidence, 1.0);
- }
- /**
- * 判断学习趋势
- */
- private function determineTrend(array $attempts): string
- {
- if (count($attempts) < 3) {
- return 'insufficient'; // 数据不足
- }
- // 按时间排序
- usort($attempts, function($a, $b) {
- $timeA = strtotime($a['completed_at'] ?? $a['created_at'] ?? 0);
- $timeB = strtotime($b['completed_at'] ?? $b['created_at'] ?? 0);
- return $timeA <=> $timeB;
- });
- // 分为前后两部分
- $midPoint = intdiv(count($attempts), 2);
- $firstHalf = array_slice($attempts, 0, $midPoint);
- $secondHalf = array_slice($attempts, $midPoint);
- $firstHalfAccuracy = $this->calculateAccuracyRate($firstHalf);
- $secondHalfAccuracy = $this->calculateAccuracyRate($secondHalf);
- $improvement = $secondHalfAccuracy - $firstHalfAccuracy;
- if ($improvement > 0.1) {
- return 'improving'; // 提升
- } elseif ($improvement < -0.1) {
- return 'declining'; // 下降
- } else {
- return 'stable'; // 稳定
- }
- }
- /**
- * 获取学生的答题记录
- */
- private function getStudentAttempts(string $studentId, string $kpCode): array
- {
- $attempts = DB::table('student_attempts')
- ->where('student_id', $studentId)
- ->where('kp_code', $kpCode)
- ->orderBy('created_at', 'asc')
- ->get();
- return $attempts->map(function ($item) {
- return (array) $item;
- })->toArray();
- }
- /**
- * 批量更新学生掌握度
- */
- public function batchUpdateMastery(string $studentId, array $kpCodes): array
- {
- $results = [];
- foreach ($kpCodes as $kpCode) {
- $masteryData = $this->calculateMasteryLevel($studentId, $kpCode);
- // 保存到数据库
- DB::table('student_knowledge_mastery')
- ->updateOrInsert(
- ['student_id' => $studentId, 'kp_code' => $kpCode],
- [
- 'mastery_level' => $masteryData['mastery'],
- 'confidence_level' => $masteryData['confidence'],
- 'total_attempts' => $masteryData['total_attempts'],
- 'correct_attempts' => $masteryData['correct_attempts'],
- 'mastery_trend' => $masteryData['trend'],
- 'last_mastery_update' => now(),
- 'updated_at' => now(),
- ]
- );
- $results[$kpCode] = $masteryData;
- }
- return $results;
- }
- /**
- * 获取学生所有知识点的掌握度概览
- */
- public function getStudentMasteryOverview(string $studentId): array
- {
- $masteryList = DB::table('student_knowledge_mastery')
- ->where('student_id', $studentId)
- ->get();
- if ($masteryList->isEmpty()) {
- return [
- 'total_knowledge_points' => 0,
- 'average_mastery_level' => 0.0,
- 'mastered_knowledge_points' => 0,
- 'good_knowledge_points' => 0,
- 'weak_knowledge_points' => 0,
- 'weak_knowledge_points_list' => [],
- 'details' => [],
- ];
- }
- $masteryArray = $masteryList->toArray();
- $total = count($masteryArray);
- $average = $masteryArray ? array_sum(array_column($masteryArray, 'mastery_level')) / $total : 0;
- $mastered = [];
- $good = [];
- $weak = [];
- foreach ($masteryArray as $item) {
- $level = floatval($item->mastery_level);
- if ($level >= self::MASTERY_THRESHOLD_MASTER) {
- $mastered[] = $item;
- } elseif ($level >= self::MASTERY_THRESHOLD_GOOD) {
- $good[] = $item;
- } else {
- $weak[] = $item;
- }
- }
- return [
- 'total_knowledge_points' => $total,
- 'average_mastery_level' => round($average, 4),
- 'mastered_knowledge_points' => count($mastered),
- 'good_knowledge_points' => count($good),
- 'weak_knowledge_points' => count($weak),
- 'weak_knowledge_points_list' => $weak,
- 'details' => $masteryArray,
- ];
- }
- }
|