Index: lams_central/src/java/org/lamsfoundation/lams/web/qb/QbStatsController.java =================================================================== diff -u -r75e43eb7dffa6bf6eb2a11bb1e616c53bdbd76ed -raccfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb --- lams_central/src/java/org/lamsfoundation/lams/web/qb/QbStatsController.java (.../QbStatsController.java) (revision 75e43eb7dffa6bf6eb2a11bb1e616c53bdbd76ed) +++ lams_central/src/java/org/lamsfoundation/lams/web/qb/QbStatsController.java (.../QbStatsController.java) (revision accfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb) @@ -47,7 +47,7 @@ @RequestMapping("/show") public String showStats(@RequestParam long qbQuestionUid, Model model) throws Exception { - QbStatsDTO stats = qbService.getStats(qbQuestionUid); + QbStatsDTO stats = qbService.getQbQuestionStats(qbQuestionUid); model.addAttribute("stats", stats); return "qb/stats"; } Index: lams_central/web/qb/stats.jsp =================================================================== diff -u -re31315857f6ab9eeb9fc2d2719c558255e1e202f -raccfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb --- lams_central/web/qb/stats.jsp (.../stats.jsp) (revision e31315857f6ab9eeb9fc2d2719c558255e1e202f) +++ lams_central/web/qb/stats.jsp (.../stats.jsp) (revision accfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb) @@ -186,8 +186,17 @@ Tool type - Average correct selection
(as first choice) + Test participant count + + Difficulty index + + + Discrimination index + + + Point biserial + @@ -205,15 +214,21 @@ - - - - - - - % - - + + + + - + - + - + + + + + + + + Index: lams_common/src/java/org/lamsfoundation/lams/commonContext.xml =================================================================== diff -u -r2e9ee1c2451a05981f05e9edf89bb3356cf2b147 -raccfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb --- lams_common/src/java/org/lamsfoundation/lams/commonContext.xml (.../commonContext.xml) (revision 2e9ee1c2451a05981f05e9edf89bb3356cf2b147) +++ lams_common/src/java/org/lamsfoundation/lams/commonContext.xml (.../commonContext.xml) (revision accfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb) @@ -540,6 +540,7 @@ + Index: lams_common/src/java/org/lamsfoundation/lams/dbupdates/patch20190110.sql =================================================================== diff -u -rea713460dca019bef05427df59a6609f89285656 -raccfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb --- lams_common/src/java/org/lamsfoundation/lams/dbupdates/patch20190110.sql (.../patch20190110.sql) (revision ea713460dca019bef05427df59a6609f89285656) +++ lams_common/src/java/org/lamsfoundation/lams/dbupdates/patch20190110.sql (.../patch20190110.sql) (revision accfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb) @@ -524,6 +524,13 @@ AND qo.qb_question_uid = tq.qb_question_uid AND o.question_uid = tq.tool_question_uid; +UPDATE tl_laasse10_option_answer AS sa, tl_laasse10_question_option AS o, lams_qb_tool_question AS tq, lams_qb_option AS qo + SET sa.question_option_uid = qo.uid + WHERE o.sequence_id = qo.display_order + AND sa.question_option_uid = o.uid + AND qo.qb_question_uid = tq.qb_question_uid + AND o.question_uid = tq.tool_question_uid; + -- prepare for answer inheritance INSERT INTO lams_qb_tool_answer SELECT uid, assessment_question_uid, submitted_option_uid FROM tl_laasse10_question_result; Index: lams_common/src/java/org/lamsfoundation/lams/qb/dao/IQbDAO.java =================================================================== diff -u -r0d764ec63c5013349de051818fbc82a970d6a803 -raccfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb --- lams_common/src/java/org/lamsfoundation/lams/qb/dao/IQbDAO.java (.../IQbDAO.java) (revision 0d764ec63c5013349de051818fbc82a970d6a803) +++ lams_common/src/java/org/lamsfoundation/lams/qb/dao/IQbDAO.java (.../IQbDAO.java) (revision accfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb) @@ -37,6 +37,8 @@ Map getAnswerStatsForActivity(long activityId); + Map getAnswersForActivity(long activityId, long qbQuestionUid); + Map getBurningQuestions(long qbQuestionUid); List getPagedQbQuestions(Integer questionType, int page, int size, String sortBy, String sortOrder, Index: lams_common/src/java/org/lamsfoundation/lams/qb/dao/hibernate/QbDAO.java =================================================================== diff -u -rdb82bbb0a1dbdd3de23181aa6da4595d836e274b -raccfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb --- lams_common/src/java/org/lamsfoundation/lams/qb/dao/hibernate/QbDAO.java (.../QbDAO.java) (revision db82bbb0a1dbdd3de23181aa6da4595d836e274b) +++ lams_common/src/java/org/lamsfoundation/lams/qb/dao/hibernate/QbDAO.java (.../QbDAO.java) (revision accfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb) @@ -1,5 +1,6 @@ package org.lamsfoundation.lams.qb.dao.hibernate; +import java.math.BigInteger; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -26,6 +27,20 @@ + "WHERE a.qbToolQuestion.uid = :qbToolQuestionUid GROUP BY a.qbOption.uid"; private static final String FIND_ANSWER_STATS_BY_ACTIVITY = "SELECT a.qbOption.uid, COUNT(a.uid) FROM QbToolAnswer AS a, " + " ToolActivity AS act WHERE a.qbToolQuestion.toolContentId = act.toolContentId AND act.activityId = :activityId GROUP BY a.qbOption.uid"; + private static final String FIND_ANSWERS_BY_ACTIVITY = "SELECT COALESCE(mcu.que_usr_id, su.user_id, au.user_id), " + + "COALESCE(a.qb_option_uid, aa.question_option_uid) AS opt " + + "FROM lams_learning_activity AS act JOIN lams_qb_tool_question AS tq USING (tool_content_id) " + + "JOIN lams_qb_tool_answer AS a USING (tool_question_uid) " + + "LEFT JOIN tl_lamc11_usr_attempt AS mca ON a.answer_uid = mca.uid " + + "LEFT JOIN tl_lamc11_que_usr AS mcu ON mca.que_usr_id = mcu.uid " + + "LEFT JOIN tl_lascrt11_answer_log AS sa ON a.answer_uid = sa.uid " + + "LEFT JOIN tl_lascrt11_session AS ss ON sa.session_id = ss.session_id " + + "LEFT JOIN tl_lascrt11_user AS su ON ss.uid = su.session_uid " + + "LEFT JOIN tl_laasse10_option_answer AS aa ON a.answer_uid = aa.question_result_uid AND aa.answer_boolean = 1 " + + "LEFT JOIN tl_laasse10_question_result AS aq ON a.answer_uid = aq.uid " + + "LEFT JOIN tl_laasse10_assessment_result AS ar ON aq.result_uid = ar.uid " + + "LEFT JOIN tl_laasse10_user AS au ON ar.user_uid = au.uid " + + "WHERE act.activity_id = :activityId AND tq.qb_question_uid = :qbQuestionUid HAVING opt IS NOT NULL"; private static final String FIND_BURNING_QUESTIONS = "SELECT b.question, COUNT(bl.uid) FROM ScratchieBurningQuestion b LEFT OUTER JOIN " + "BurningQuestionLike AS bl ON bl.burningQuestion = b WHERE b.scratchieItem.qbQuestion.uid = :qbQuestionUid " + "GROUP BY b.question ORDER BY COUNT(bl.uid) DESC"; @@ -199,6 +214,18 @@ @Override @SuppressWarnings("unchecked") + public Map getAnswersForActivity(long activityId, long qbQuestionUid) { + List result = this.getSession().createSQLQuery(FIND_ANSWERS_BY_ACTIVITY) + .setParameter("activityId", activityId).setParameter("qbQuestionUid", qbQuestionUid).list(); + Map map = new HashMap<>(result.size()); + for (Object[] answerStat : result) { + map.put(((BigInteger) answerStat[0]).intValue(), ((BigInteger) answerStat[1]).longValue()); + } + return map; + } + + @Override + @SuppressWarnings("unchecked") public Map getBurningQuestions(long qbQuestionUid) { List result = this.getSession().createQuery(FIND_BURNING_QUESTIONS) .setParameter("qbQuestionUid", qbQuestionUid).list(); Index: lams_common/src/java/org/lamsfoundation/lams/qb/dto/QbStatsActivityDTO.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/qb/dto/QbStatsActivityDTO.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/qb/dto/QbStatsActivityDTO.java (revision accfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb) @@ -0,0 +1,52 @@ +package org.lamsfoundation.lams.qb.dto; + +import org.lamsfoundation.lams.learningdesign.ToolActivity; + +public class QbStatsActivityDTO { + public ToolActivity activity; + public Integer participantCount; + public Double difficultyIndex; + public Double discriminationIndex; + public Double pointBiserial; + + public ToolActivity getActivity() { + return activity; + } + + public void setActivity(ToolActivity activity) { + this.activity = activity; + } + + public Integer getParticipantCount() { + return participantCount; + } + + public void setParticipantCount(Integer testParticipantCount) { + this.participantCount = testParticipantCount; + } + + public Double getDifficultyIndex() { + return difficultyIndex; + } + + public void setDifficultyIndex(Double difficultyIndex) { + this.difficultyIndex = difficultyIndex; + } + + public Double getDiscriminationIndex() { + return discriminationIndex; + } + + public void setDiscriminationIndex(Double discriminationIndex) { + this.discriminationIndex = discriminationIndex; + } + + public Double getPointBiserial() { + return pointBiserial; + } + + public void setPointBiserial(Double pointBiserial) { + this.pointBiserial = pointBiserial; + } + +} \ No newline at end of file Index: lams_common/src/java/org/lamsfoundation/lams/qb/dto/QbStatsDTO.java =================================================================== diff -u -r28523d629738623026587908460f0ec8268c5f6f -raccfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb --- lams_common/src/java/org/lamsfoundation/lams/qb/dto/QbStatsDTO.java (.../QbStatsDTO.java) (revision 28523d629738623026587908460f0ec8268c5f6f) +++ lams_common/src/java/org/lamsfoundation/lams/qb/dto/QbStatsDTO.java (.../QbStatsDTO.java) (revision accfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb) @@ -3,31 +3,9 @@ import java.util.List; import java.util.Map; -import org.lamsfoundation.lams.learningdesign.ToolActivity; import org.lamsfoundation.lams.qb.model.QbQuestion; public class QbStatsDTO { - public static class QbStatsActivityDTO { - public ToolActivity activity; - public Integer average; - - public ToolActivity getActivity() { - return activity; - } - - public void setActivity(ToolActivity activity) { - this.activity = activity; - } - - public Integer getAverage() { - return average; - } - - public void setAverage(Integer average) { - this.average = average; - } - } - private QbQuestion question; private Map answersRaw; private Map answersPercent; Index: lams_common/src/java/org/lamsfoundation/lams/qb/service/IQbService.java =================================================================== diff -u -r394ff1771926ee7d32cc7eca41bf83fc964c2ebe -raccfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb --- lams_common/src/java/org/lamsfoundation/lams/qb/service/IQbService.java (.../IQbService.java) (revision 394ff1771926ee7d32cc7eca41bf83fc964c2ebe) +++ lams_common/src/java/org/lamsfoundation/lams/qb/service/IQbService.java (.../IQbService.java) (revision accfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb) @@ -1,11 +1,11 @@ package org.lamsfoundation.lams.qb.service; -import org.lamsfoundation.lams.qb.dto.QbStatsDTO; import java.util.List; +import org.lamsfoundation.lams.qb.dto.QbStatsActivityDTO; +import org.lamsfoundation.lams.qb.dto.QbStatsDTO; import org.lamsfoundation.lams.qb.model.QbQuestion; - public interface IQbService { // statuses of comparing QB question coming from authoring with data existing in DB @@ -18,13 +18,15 @@ static final int QUESTION_MODIFIED_VERSION_BUMP = 2; // it is a new question static final int QUESTION_MODIFIED_ID_BUMP = 3; - + + static final double STATS_TOP_BOTTOM_GROUP_SIZE = 0.27; + /** * @param qbQuestionUid * @return QbQuestion object with the specified uid */ QbQuestion getQbQuestionByUid(Long qbQuestionUid); - + /** * @param questionId * @return questions sharing the same questionId @@ -37,10 +39,14 @@ // finds next version for given question ID for Question Bank question int getMaxQuestionVersion(Integer qbQuestionId); - QbStatsDTO getStats(long qbQuestionUid); - - List getPagedQbQuestions(Integer questionType, int page, int size, String sortBy, - String sortOrder, String searchString); - + QbStatsDTO getQbQuestionStats(long qbQuestionUid); + + QbStatsActivityDTO getActivityStats(Long activityId, Long qbQuestionUid); + + QbStatsActivityDTO getActivityStats(Long activityId, Long qbQuestionUid, Long correctOptionUid); + + List getPagedQbQuestions(Integer questionType, int page, int size, String sortBy, String sortOrder, + String searchString); + int getCountQbQuestions(Integer questionType, String searchString); } Index: lams_common/src/java/org/lamsfoundation/lams/qb/service/QbService.java =================================================================== diff -u -r0d764ec63c5013349de051818fbc82a970d6a803 -raccfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb --- lams_common/src/java/org/lamsfoundation/lams/qb/service/QbService.java (.../QbService.java) (revision 0d764ec63c5013349de051818fbc82a970d6a803) +++ lams_common/src/java/org/lamsfoundation/lams/qb/service/QbService.java (.../QbService.java) (revision accfdd9d97b1e9f4db2c48861a2f5ea1d065a3cb) @@ -1,14 +1,20 @@ package org.lamsfoundation.lams.qb.service; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import org.lamsfoundation.lams.gradebook.GradebookUserLesson; +import org.lamsfoundation.lams.gradebook.service.IGradebookService; import org.lamsfoundation.lams.learningdesign.ToolActivity; import org.lamsfoundation.lams.qb.dao.IQbDAO; +import org.lamsfoundation.lams.qb.dto.QbStatsActivityDTO; import org.lamsfoundation.lams.qb.dto.QbStatsDTO; -import org.lamsfoundation.lams.qb.dto.QbStatsDTO.QbStatsActivityDTO; import org.lamsfoundation.lams.qb.model.QbOption; import org.lamsfoundation.lams.qb.model.QbQuestion; import org.lamsfoundation.lams.util.WebUtil; @@ -20,12 +26,14 @@ public class QbService implements IQbService { private IQbDAO qbDAO; - + + private IGradebookService gradebookService; + @Override public QbQuestion getQbQuestionByUid(Long qbQuestionUid) { return qbDAO.getQbQuestionByUid(qbQuestionUid); } - + @Override public List getQbQuestionsByQuestionId(Integer questionId) { return qbDAO.getQbQuestionsByQuestionId(questionId); @@ -40,20 +48,20 @@ public int getMaxQuestionVersion(Integer qbQuestionId) { return qbDAO.getMaxQuestionVersion(qbQuestionId); } - + @Override public List getPagedQbQuestions(Integer questionType, int page, int size, String sortBy, String sortOrder, String searchString) { return qbDAO.getPagedQbQuestions(questionType, page, size, sortBy, sortOrder, searchString); } - + @Override public int getCountQbQuestions(Integer questionType, String searchString) { return qbDAO.getCountQbQuestions(questionType, searchString); } @Override - public QbStatsDTO getStats(long qbQuestionUid) { + public QbStatsDTO getQbQuestionStats(long qbQuestionUid) { QbStatsDTO stats = new QbStatsDTO(); QbQuestion qbQuestion = (QbQuestion) qbDAO.find(QbQuestion.class, qbQuestionUid); List qbOptions = qbQuestion.getQbOptions(); @@ -63,24 +71,18 @@ List activities = qbDAO.getQuestionActivities(qbQuestionUid); List activityDTOs = new LinkedList<>(); + + Long correctOptionUid = null; + for (QbOption option : qbOptions) { + if (option.isCorrect()) { + correctOptionUid = option.getUid(); + break; + } + } // calculate correct answer average for each activity for (ToolActivity activity : activities) { - QbStatsActivityDTO activityDTO = new QbStatsActivityDTO(); - activityDTO.setActivity(activity); - Map activityAnswersRaw = qbDAO.getAnswerStatsForActivity(activity.getActivityId()); - double total = 0; - long correctCount = 0; - for (QbOption option : qbOptions) { - Long answerCount = activityAnswersRaw.get(option.getUid()); - if (answerCount == null) { - answerCount = 0L; - } - total += answerCount; - if (option.isCorrect()) { - correctCount = answerCount; - } - } - activityDTO.setAverage(total == 0 ? null : Long.valueOf(Math.round(correctCount / total * 100)).intValue()); + QbStatsActivityDTO activityDTO = getActivityStats(activity.getActivityId(), qbQuestionUid, + correctOptionUid); activityDTOs.add(activityDTO); } stats.setActivities(activityDTOs); @@ -114,7 +116,102 @@ return stats; } + @Override + public QbStatsActivityDTO getActivityStats(Long activityId, Long qbQuestionUid) { + QbQuestion qbQuestion = (QbQuestion) qbDAO.find(QbQuestion.class, qbQuestionUid); + for (QbOption option : qbQuestion.getQbOptions()) { + if (option.isCorrect()) { + return getActivityStats(activityId, qbQuestionUid, option.getUid()); + } + } + return null; + } + + @Override + public QbStatsActivityDTO getActivityStats(Long activityId, Long qbQuestionUid, Long correctOptionUid) { + ToolActivity activity = (ToolActivity) qbDAO.find(ToolActivity.class, activityId); + Long lessonId = activity.getLearningDesign().getLessons().iterator().next().getLessonId(); + List userLessonGrades = gradebookService.getGradebookUserLesson(lessonId); + int participantCount = userLessonGrades.size(); + + QbStatsActivityDTO activityDTO = new QbStatsActivityDTO(); + activityDTO.setActivity(activity); + activityDTO.setParticipantCount(participantCount); + + // if there is only 1 participant, there is no point in calculating question indexes + if (participantCount > 1) { + // mapping of user ID -> option UID + Map activityAnswers = qbDAO.getAnswersForActivity(activity.getActivityId(), qbQuestionUid); + // see who answered correctly + Set correctUserIds = new HashSet<>(); + for (Entry answer : activityAnswers.entrySet()) { + Integer userId = answer.getKey(); + Long optionUid = answer.getValue(); + if (correctOptionUid.equals(optionUid)) { + correctUserIds.add(userId); + } + } + + int correctUserCount = correctUserIds.size(); + int incorrectUserCount = participantCount - correctUserCount; + int topUsersCorrect = 0; + int bottomUsersCorrect = 0; + double allUserMarkSum = 0; + double correctUserMarkSum = 0; + double incorrectUserMarkSum = 0; + + // sort grades by highest mark + Collections.sort(userLessonGrades, (a, b) -> a.getMark().compareTo(b.getMark())); + // see how many learners should be in top/bottom 27% of the group + int groupCount = (int) Math.ceil(STATS_TOP_BOTTOM_GROUP_SIZE * participantCount); + + // go through each grade and gather data for indexes + for (int userIndex = 0; userIndex < participantCount; userIndex++) { + GradebookUserLesson grade = userLessonGrades.get(userIndex); + double mark = grade.getMark(); + allUserMarkSum += mark; + boolean isCorrect = correctUserIds.contains(grade.getLearner().getUserId()); + if (isCorrect) { + correctUserMarkSum += mark; + if (userIndex < groupCount) { + topUsersCorrect++; + } else if (userIndex >= participantCount - groupCount) { + bottomUsersCorrect++; + } + } else { + incorrectUserMarkSum += mark; + } + } + + // calculate standard deviation needed for point biserial + double deviation = 0; + double markAverage = allUserMarkSum / participantCount; + for (int userIndex = 0; userIndex < participantCount; userIndex++) { + GradebookUserLesson grade = userLessonGrades.get(userIndex); + double mark = grade.getMark(); + deviation += Math.pow(mark - markAverage, 2); + } + deviation = Math.sqrt(deviation / participantCount); + + activityDTO.setDifficultyIndex(participantCount == 0 ? null : (double) correctUserCount / participantCount); + activityDTO.setDiscriminationIndex((double) (topUsersCorrect - bottomUsersCorrect) / groupCount); + if (correctUserCount == 0 || incorrectUserCount == 0) { + activityDTO.setPointBiserial(0d); + } else { + activityDTO.setPointBiserial( + (correctUserMarkSum / correctUserCount - incorrectUserMarkSum / incorrectUserCount) / deviation + * Math.sqrt(correctUserCount * incorrectUserCount / Math.pow(participantCount, 2))); + } + } + + return activityDTO; + } + public void setQbDAO(IQbDAO qbDAO) { this.qbDAO = qbDAO; } + + public void setGradebookService(IGradebookService gradebookService) { + this.gradebookService = gradebookService; + } } \ No newline at end of file