public function mappingsAction() { echo "<pre>"; echo "All units\n"; print_r(MappingHelper::allUnits()); echo "<hr>Concepts in unit 4\n"; print_r(MappingHelper::conceptsInUnit("4")); echo "<hr>Questions in concept (lecture number) 1\n"; print_r(MappingHelper::questionsInConcept("1")); echo "<hr>Question info for question 78.4\n"; print_r(MappingHelper::questionInformation("78.4")); echo "<hr>Videos for concept 9\n"; print_r(MappingHelper::videosForConcept("9")); echo "<hr>Resources for concept 1\n"; print_r(MappingHelper::resourcesForConcept("1")); echo "<hr>Concepts within the past 2 weeks: \n"; print_r(MappingHelper::conceptsWithin2Weeks()); }
public function scatterplotAction($scope = 'concept', $groupingId = '', $debug = false) { $this->view->disable(); // Get our context (this takes care of starting the session, too) $context = $this->getDI()->getShared('ltiContext'); if (!$context->valid) { echo '[{"error":"Invalid lti context"}]'; return; } // Get the list of questions associated with concepts for the given scope and grouping ID $questions = []; switch ($scope) { case "concept": // Filter based on concept $questions = MappingHelper::questionsInConcept($groupingId); break; case "unit": // Filter based on unit $questions = MappingHelper::questionsInConcepts(MappingHelper::conceptsInUnit($groupingId)); break; default: echo '[{"error":"Invalid scope option"}]'; return; break; } if ($debug) { echo "questions for scope {$scope} and grouping {$groupingId}: \n"; print_r($questions); } $classHelper = new ClassHelper(); // Array of questions with more details about each $questionDetails = array(); // Get some info about each question foreach ($questions as $question) { // Check that it's a valid question if ($question != false) { // Get number of attempts $question["attempts"] = MasteryHelper::countAttemptsForQuestion($context->getUserName(), $question["OA Quiz ID"], $question["Question Number"], $debug); $question["scaledAttemptScore"] = $classHelper->calculateScaledAttemptScoreForQuestion($question["attempts"], $question["OA Quiz ID"], $question["Question Number"], $debug); // Get amount of associated videos watched // Note that question ID is being used instead of assessment ID and question number, since we're searching the csv mapping and not dealing with assessment statements here $question["videoPercentage"] = MasteryHelper::calculateUniqueVideoPercentageForQuestion($context->getUserName(), $questionId); $questionDetails[] = $question; } } /* function randomPoint($group, $q) { // Randomly return outliers if (rand(0,30) == 5) { return [$group, $q["quizNumber"], $q["questionNumber"], rand(-10000, 1000), rand(-10000, 1000)]; } return [$group, $q["quizNumber"], $q["questionNumber"], rand(-100, 100), rand(-100, 100)]; } $result = []; // For now, return random points based on number of questions $numPoints = count($questionDetails); //foreach ($questionDetails as $q) { //$result [] = //} // for ($i=0; $i<$numPoints; $i++) { $result []= randomPoint("student", $questionDetails[$i]); for ($j=0; $j<10; $j++) { $result []= randomPoint("class", $questionDetails[$i]); } } $xValues = array_map(function($point) { return $point[3]; }, $result); $yValues = array_map(function($point) { return $point[4]; }, $result); // TODO check that xValues and yValues have a length, otherwise statshelper will spit out errors // Perform some statistics grossness // Remove any outliers for both axes, based on 1.5*IQR // Cap and floor x outliers $xStats = StatsHelper::boxPlotValues($xValues); $result = array_map(function($point) use ($xStats) { $x = $point[3]; // Floor upper outliers if ($x > $xStats['q3'] + (1.5 * $xStats['iqr'])) { $x = $xStats['q3'] + (.5 * $xStats['iqr']); } // Cap lower outliers if ($x < $xStats['q1'] - (1.5 * $xStats['iqr'])) { $x = $xStats['q1'] - (.5 * $xStats['iqr']); } $point[3] = $x; return $point; }, $result); // Scale all the scores from 0 to 10 // TODO // Cap and floor y outliers $yStats = StatsHelper::boxPlotValues($yValues); $result = array_map(function($point) use ($yStats) { $y = $point[4]; // Floor upper outliers if ($y > $yStats['q3'] + (1.5 * $yStats['iqr'])) { $y = $yStats['q3'] + (.5 * $yStats['iqr']); } // Cap lower outliers if ($y < $yStats['q1'] - (1.5 * $yStats['iqr'])) { $y = $yStats['q1'] - (.5 * $yStats['iqr']); } $point[4] = $y; return $point; }, $result); //print_r($result); //print_r($yStats); //die(); */ // X = video percentage, Y = question attempts $headerRow = ["group", "quiz_number", "question_number", "x", "y"]; $result = array_map(function ($q) { return ["student", $q["OA Quiz ID"], $q["Question Number"], $q["videoPercentage"], $q["scaledAttemptScore"]]; }, $questionDetails); if ($debug) { echo "question details for scope {$scope} and grouping {$groupingId}: \n"; print_r($questionDetails); print_r($result); } // Output data as csv so that we only have to send header information once for so many points if (!$debug) { header("Content-Type: text/csv"); } $output = fopen("php://output", "w"); fputcsv($output, $headerRow); foreach ($result as $row) { fputcsv($output, $row); // here you can change delimiter/enclosure } fclose($output); }
public function questionRecommendationsAction($scope = 'unit', $groupingId = '3', $debug = false) { $this->view->disable(); // Get our context (this takes care of starting the session, too) $context = $this->getDI()->getShared('ltiContext'); if (!$context->valid) { echo '[{"error":"Invalid lti context"}]'; return; } if (!isset($groupingId)) { echo '[{"error":"No scope grouping ID specified"}]'; return; } $classHelper = new ClassHelper(); $group1 = []; $group2 = []; $group3 = []; $group4 = []; // Get the list of questions associated with concepts for the given scope and grouping ID $questionRows = []; switch ($scope) { case "concept": // Filter based on concept $questionRows = MappingHelper::questionsInConcept($groupingId); break; case "unit": // Filter based on unit $questionRows = MappingHelper::questionsInConcepts(MappingHelper::conceptsInUnit($groupingId)); break; default: // Allowing all would take too long echo '[{"error":"Invalid scope option"}]'; return; break; } if ($debug) { echo "<pre>Getting information for these questions in scope {$scope} and ID {$groupingId}\n"; print_r($questionRows); } $questions = []; // Get some info about each question foreach ($questionRows as $question) { // Get number of attempts and number of correct attempts $question["attempts"] = MasteryHelper::countAttemptsForQuestion($context->getUserName(), $question["OA Quiz ID"], $question["Question Number"], $debug); $question["correctAttempts"] = MasteryHelper::countCorrectAttemptsForQuestion($context->getUserName(), $question["OA Quiz ID"], $question["Question Number"], $debug); // Get amount of associated videos watched $question["videoPercentage"] = MasteryHelper::calculateUniqueVideoPercentageForQuestion($context->getUserName(), $question); // Variables used in the display table // This is one place where we're just using correct, not better correct, attempts $question["correct"] = $question["correctAttempts"]["correct"] > 0; $question["classAverageAttempts"] = $classHelper->calculateAverageAttemptsForQuestion($question["OA Quiz ID"], $question["Question Number"], $debug); $question["classViewedHint"] = $classHelper->calculateViewedHintPercentageForQuestion($question["OA Quiz ID"], $question["Question Number"], $debug); $question["classViewedAnswer"] = $classHelper->calculateViewedAnswerPercentageForQuestion($question["OA Quiz ID"], $question["Question Number"], $debug); $questions[] = $question; } // Fetch question texts for all questions in these assessments // Get the Open Assessments API endpoint from config $assessmentsEndpoint = $this->getDI()->getShared('config')->openassessments_endpoint; // Extract a list of assessment IDs from our list of questions. We'll get question texts for these. $assessmentIDs = ["assessment_ids" => array_values(array_unique(array_column($questions, "OA Quiz ID")))]; $request = $assessmentsEndpoint . "api/question_text"; //$request = "http://192.168.33.102/api/question_text"; if ($debug) { echo "Fetching question texts for these assessment IDs:\n"; print_r(array_column($questions, "OA Quiz ID")); print_r($assessmentIDs); echo $request; } #print_r($assessmentIDs); #echo json_encode($request); $session = curl_init($request); curl_setopt($session, CURLOPT_RETURNTRANSFER, true); curl_setopt($session, CURLOPT_POST, 1); curl_setopt($session, CURLOPT_POSTFIELDS, json_encode($assessmentIDs)); $response = curl_exec($session); #echo json_encode($questionRows); // Catch curl errors if (curl_errno($session)) { $error = "Curl error: " . curl_error($session); } curl_close($session); #print_r($response); $questionTexts = json_decode($response, true); if ($debug) { print_r($questionTexts); } foreach ($questions as $key => $q) { // Make sure the question text exists before setting it $questions[$key]["display"] = isset($questionTexts[$q["OA Quiz ID"]][$q["Question Number"] - 1]) ? $questionTexts[$q["OA Quiz ID"]][$q["Question Number"] - 1] : "Error getting question text for #{$q["OA Quiz ID"]} # #{$q["Question Number"]}-1"; // Avoid off-by-one error. The question id from statement object id will be 1 to n+1 //$q["questionText"] = $questionText; } // Now go through the questions for each group and find matching questions foreach ($questions as $question) { // Group 1: // Questions with 0 attempts if ($question["attempts"] == 0) { $group1[] = $question; } // Group 2: // >0 attempts for each question // No correct statements without a show answer statement in the preceding minute for each question (correctAttempts < 1) // Watched less than half of the videos associated with each question if ($question["attempts"] > 0 && $question["correctAttempts"]["betterCorrect"] == 0 && $question["videoPercentage"] < 0.5) { $group2[] = $question; } // Group 3: // >0 attempts for each question // No correct statements without a show answer statement in the preceding minute for each question (correctAttempts < 1) // Watched more than half of the videos associated with each question if ($question["attempts"] > 0 && $question["correctAttempts"]["betterCorrect"] == 0 && $question["videoPercentage"] >= 0.5) { $group3[] = $question; } // Group 4: // > 0 correct statements without a show answer statement in the preceding minute for each question (correctAttempts > 0) // More than 1 attempt if ($question["correctAttempts"]["betterCorrect"] > 0 && $question["attempts"] > 1) { $group4[] = $question; } } if ($debug) { print_r($questions); } $result = ["group1" => $group1, "group2" => $group2, "group3" => $group3, "group4" => $group4]; echo json_encode($result); }
public static function calculateConceptMasteryScore($studentId, $conceptId, $debug = false) { // If the concept row was passed in, extract lecture number if (is_array($conceptId)) { $conceptId = $conceptId["Lecture Number"]; } // Get questions in concept $questions = MappingHelper::questionsInConcept($conceptId); if ($debug) { echo "<pre><h1>Concept ID {$conceptId}: </h1>"; print_r($questions); echo "<hr>"; } // Array to hold information about each question (we don't care about essay questions) $conceptShortAnswerQuestions = array(); $conceptMultipleChoiceQuestions = array(); // Loop through each question and get basic information for each foreach ($questions as $question) { switch ($question["Question Type"]) { case "essay": // Don't include essay questions in any calculations, so don't add this question to the $conceptQuestions array break; case "short_answer": // Get the number of attempts and correct (no show answer in preceding minute) attempts $question["attempts"] = self::countAttemptsForQuestion($studentId, $question["OA Quiz ID"], $question["Question Number"], $debug); $question["correctAttempts"] = self::countCorrectAttemptsForQuestion($studentId, $question["OA Quiz ID"], $question["Question Number"], $debug); $conceptShortAnswerQuestions[] = $question; break; case "multiple_choice": // Get the number of attempts and correct (no show answer in preceding minute) attempts $question["attempts"] = self::countAttemptsForQuestion($studentId, $question["OA Quiz ID"], $question["Question Number"], $debug); $question["correctAttempts"] = self::countCorrectAttemptsForQuestion($studentId, $question["OA Quiz ID"], $question["Question Number"], $debug); $conceptMultipleChoiceQuestions[] = $question; break; } } // 1. Short answer questions // Total number of short answer questions $shortAnswerQuestionCount = count($conceptShortAnswerQuestions); // Avoid division by zero if no questions of this type (set scores to 0 for this part, and move on) if ($shortAnswerQuestionCount > 0) { // 1.a. Calculate initial: correctness factor and attempts penalty // Get number of correct short answer questions // Use array_filter to get short answer questions with correct attempts > 0 $shortAnswerCorrectCount = count(array_filter($conceptShortAnswerQuestions, function ($question) { return $question["correctAttempts"]["betterCorrect"] > 0; })); // Get total number of attempts for all short answer questions $shortAnswerAttemptCount = array_sum(array_column($conceptShortAnswerQuestions, "attempts")); // TODO refactor magic numbers // Short answer initial = ( 10 * ( # correct SA in concept / # total SA questions in concept) - (0.2 * (total attempts for all SA questions in concept - number of SA questions in concept) / number of SA questions in concept) ) $shortAnswerCorrectnessFactor = 10 * ($shortAnswerCorrectCount / $shortAnswerQuestionCount); $shortAnswerAttemptPenalty = 0.2 * max($shortAnswerAttemptCount - $shortAnswerQuestionCount, 0) / $shortAnswerQuestionCount; $shortAnswerInitialScore = max($shortAnswerCorrectnessFactor - $shortAnswerAttemptPenalty, 0); if ($debug) { echo "Short answer correctness and attempts penalty: "; // TODO avoid division by zero if no questions of this type // Use max to make sure we don't go below 0 echo "(10 * ( {$shortAnswerCorrectCount} / {$shortAnswerQuestionCount}) ) - (0.2 * max({$shortAnswerAttemptCount} - {$shortAnswerQuestionCount}, 0) / {$shortAnswerQuestionCount}) = {$shortAnswerInitialScore} \n"; } // 1.b. Calculate practice bonus // For questions with > 1 correct statement without a show answer statement in the preceding minute, then add a practice bonus equal to the attempts penalty. // 2.a. Short answer questions $shortAnswerPracticeBonus = 0; // Get short answer questions that have > 1 correct attempt and add a practice bonus for each of them foreach ($conceptShortAnswerQuestions as $question) { if ($question["correctAttempts"]["betterCorrect"] > 1) { $shortAnswerPracticeBonus += 0.2 * ($question["attempts"] - 1) / $shortAnswerQuestionCount; } } if ($debug) { echo "Short answer practice bonus: {$shortAnswerPracticeBonus} \n"; } } else { $shortAnswerInitialScore = 0; $shortAnswerPracticeBonus = 0; } // 2 Multiple choice questions // Total number of multiple choice questions $multipleChoiceQuestionCount = count($conceptMultipleChoiceQuestions); // Avoid division by zero if no questions of this type (set scores to 0 for this part, and move on) if ($multipleChoiceQuestionCount > 0) { // 2.a. Calculate initial: correctness factor and attempts penalty // Get number of correct multiple choice questions // Use array_filter to get multiple choice questions with correct attempts > 0 $multipleChoiceCorrectCount = count(array_filter($conceptMultipleChoiceQuestions, function ($question) { return $question["correctAttempts"]["betterCorrect"] > 0; })); // Get total number of attempts for all multiple choice questions $multipleChoiceAttemptCount = array_sum(array_column($conceptMultipleChoiceQuestions, "attempts")); // TODO refactor magic numbers // Multiple choice initial = 10 * ( # correct MC in concept / # total MC questions in concept) - ( ( sum( (question attempts - 1) * (10 / options in question) ) ) / number of MC questions $multipleChoiceCorrectnessFactor = 10 * ($multipleChoiceCorrectCount / $multipleChoiceQuestionCount); // Attempts penalty based on number of attempts for a question as well as how many options that question has $multipleChoiceAttemptPenalty = 0; // Sum of penalties foreach ($conceptMultipleChoiceQuestions as $question) { $multipleChoiceAttemptPenalty += max($question["attempts"] - 1, 0) * (10 / $question["Multiple Choice Options"]); } // Now average penalty $multipleChoiceAttemptPenalty = $multipleChoiceAttemptPenalty / $multipleChoiceQuestionCount; $multipleChoiceInitialScore = max($multipleChoiceCorrectnessFactor - $multipleChoiceAttemptPenalty, 0); if ($debug) { echo "Multiple Choice correctness and attempts penalty: "; echo "(10 * ( {$multipleChoiceCorrectCount} / {$multipleChoiceQuestionCount}) ) - ({$multipleChoiceAttemptPenalty} / {$multipleChoiceQuestionCount}) = {$multipleChoiceInitialScore} \n"; } // 2.b. Calculate practice bonus $multipleChoicePracticeBonus = 0; // Get number of multiple choice questions that have > 1 correct attempt and add a practice bonus for each of them foreach ($conceptMultipleChoiceQuestions as $question) { if ($question["correctAttempts"]["betterCorrect"] > 1) { $multipleChoicePracticeBonus += ($question["attempts"] - 1) * (10 / $question["Multiple Choice Options"]) / $multipleChoiceQuestionCount; } } if ($debug) { echo "Multiple Choice practice bonus: {$multipleChoicePracticeBonus} \n"; } } else { $multipleChoiceInitialScore = 0; $multipleChoicePracticeBonus = 0; } // 3. Calculate total mastery score for each question type: initial + bonus $shortAnswerScore = $shortAnswerInitialScore + $shortAnswerPracticeBonus; $multipleChoiceScore = $multipleChoiceInitialScore + $multipleChoicePracticeBonus; // Weight each question type score by number of questions. // Don't use total number of questions, since that will include essay. Instead use MC count + SA coconut // Avoid division by 0 (a concept might only have essay questions?) if ($shortAnswerQuestionCount + $multipleChoiceQuestionCount > 0) { $weightedShortAnswerScore = $shortAnswerScore * ($shortAnswerQuestionCount / ($shortAnswerQuestionCount + $multipleChoiceQuestionCount)); $weightedMultipleChoiceScore = $multipleChoiceScore * ($multipleChoiceQuestionCount / ($shortAnswerQuestionCount + $multipleChoiceQuestionCount)); } else { $weightedShortAnswerScore = 0; $weightedMultipleChoiceScore = 0; } // Finally! $conceptScore = $weightedShortAnswerScore + $weightedMultipleChoiceScore; if ($debug) { echo "Short answer score: {$shortAnswerScore} = {$shortAnswerInitialScore} + {$shortAnswerPracticeBonus} \n"; echo "Multiple choice score: {$multipleChoiceScore} = {$multipleChoiceInitialScore} + {$multipleChoicePracticeBonus} \n"; echo "Weighted SA: {$weightedShortAnswerScore} = {$shortAnswerScore} * ({$shortAnswerQuestionCount} / ({$shortAnswerQuestionCount} + {$multipleChoiceQuestionCount}) ) \n"; echo "Weighted MC: {$weightedMultipleChoiceScore} = {$multipleChoiceScore} * ({$multipleChoiceQuestionCount} / ({$shortAnswerQuestionCount} + {$multipleChoiceQuestionCount}) ) \n"; echo "Total concept score: {$conceptScore} \n"; echo "Questions in concept {$conceptId} <hr><pre>"; print_r($conceptShortAnswerQuestions); print_r($conceptMultipleChoiceQuestions); } // Round to 2 decimal places return round($conceptScore * 100) / 100; }