#+TITLE: dev.metalisp.survey Documentation #+AUTHOR: Marcus Kammer #+EMAIL: marcus.kammer@metalisp.dev #+DATE: [2024-10-12 21:58] #+LANGUAGE: en #+EXPORT_FILE_NAME: dev-metalisp-survey-docs #+STARTUP: nohideblocks overview * Assessment :PROPERTIES: :header-args:lisp: :exports both :eval never-export :END: ** User Requests #+begin_src http :pretty :exports code :results html GET http://localhost:8080/survey/1057733929973421737730 #+end_src #+RESULTS: #+begin_export html Survey Details
Skip to main content

Survey Details

Properties

ID: 1057733929973421737730

title
dasdas
description
fsdfsdfs
questionnaire
Open Questionnaire /de/sus
questionnaire
Open Questionnaire /de/visawi

Assesments

VISAWI Results

Individual Analysis

TIMESTAMP CRAFTSMANSHIP COLORFULNESS DIVERSITY SIMPLICITY OVERALL
2024-10-10 10:41:34 4.25 4.25 4.00 3.60 4.03
2024-10-10 10:42:01 3.75 4.50 4.40 5.60 4.56

Group Analysis

NAME MEDIAN MEAN SD MIN MAX
craftsmanship 4.00 4.00 0.35 3.75 4.25
colorfulness 4.38 4.38 0.18 4.25 4.50
diversity 4.20 4.20 0.28 4.00 4.40
simplicity 4.60 4.60 1.41 3.60 5.60
overall 4.29 4.29 0.38 4.03 4.56

SUS Results

Timestamp Q1 Q2 Q3 Q4 Q5 Q6 Q7 Q8 Q9 Q10 Score
2024-07-14 21:17:21 1.00 2.00 4.00 3.00 1.00 5.00 1.00 2.00 4.00 3.00 40.00
#+end_export #+begin_src http :pretty :exports code :results html GET http://localhost:8080/survey/252366393230629784388 #+end_src #+RESULTS: #+begin_export html Survey Details
Skip to main content

Survey Details

Properties

ID: 252366393230629784388

title
test2
description
test2
questionnaire
Open Questionnaire /de/sus
questionnaire
Open Questionnaire /de/visawi

Assesments

SUS Results

Timestamp Q1 Q2 Q3 Q4 Q5 Q6 Q7 Q8 Q9 Q10 Score
2024-10-10 18:15:27 3.00 2.00 3.00 4.00 4.00 4.00 1.00 2.00 4.00 3.00 50.00
#+end_export ** Class Definition #+name: class-diagram-assessment #+begin_src plantuml :file class-diagram-assessment.png @startuml skinparam dpi 300 package ml-survey/assessment { abstract class Assessment { # results: List # responses: List + displayer: Displayer + calculator: Calculator + calc-results(): List + results-html(): List } class SusAssessment extends Assessment {} class VisawiAssessment extends Assessment {} abstract class Displayer { + generate-html(): List } class SusDisplayer extends Displayer {} class VisawiDisplayer extends Displayer {} SusAssessment *-- SusDisplayer VisawiAssessment *-- VisawiDisplayer abstract class Calculator { + calc-results(): List + group-stats(): List } class SusCalculator extends Calculator {} class VisawiCalculator extends Calculator {} SusAssessment *-- SusCalculator VisawiAssessment *-- VisawiCalculator } @enduml #+end_src #+name: fig:class-diagram-assessment #+RESULTS: class-diagram-assessment [[file:class-diagram-assessment.png]] The only functions which should be exported from the ~ml-survey/assessment~ package are: - assessment-results-html :: Enables the caller to get the HTML for the right object. - parse-assessments :: Returns a list of assessment objects created based on a PLIST of responses. #+name: assessment-package #+begin_src lisp :tangle ./src/assessment.lisp (defpackage :ml-survey/assessment (:use #:cl) (:import-from #:ml-sbt/section #:with-section #:with-title-bar) (:import-from #:ml-sbt/tbl #:render-tbl #:render-kwlist-tbl #:render-nested-plist-tbl) (:import-from #:ml-survey/stats #:calculate-statistics #:mean) (:export #:assessment-results-html #:parse-assessments)) (in-package :ml-survey/assessment) #+end_src A assessment helps us to process the questionnaire responses and provides users with calculated results. To handle questionnaire responses we need to define a specific assessment for a specific questionnaire because questionnaires often needs specific statistical calculations. For instance, the calculations for a SUS is different from the calculations for a VISAWI. #+name: assessment-class #+begin_src lisp :tangle ./src/assessment.lisp (defclass assessment () ((results :initarg :results :reader assessment-results :type list :documentation "Complete output of an assessment.") (responses :initarg :responses :reader assessment-responses :type list :documentation "Data sent by the user using a HTML form.") (displayer :initarg :displayer :accessor assessment-displayer :type displayer) (calculator :initarg :calculator :accessor assessment-calculator :type calculator)) (:documentation "Provides mechanism to handle data related to assessments. An assessment includes the calculated results for a specific questionnaire.")) #+end_src #+name: assessment-calc-results #+begin_src lisp :tangle ./src/assessment.lisp (defmethod assessment-calc-results ((a assessment)) "Calculate results from responses." (let* ((responses (assessment-responses a)) (calculator (assessment-calculator a)) (results (calculator-calc-results calculator responses))) (make-instance (class-name (class-of a)) :results results))) #+end_src #+name: assessment-results-html #+begin_src lisp :tangle ./src/assessment.lisp (defmethod assessment-results-html ((a assessment)) "Render HTML to show results." (let* ((results (assessment-results (assessment-calc-results a))) (group-stats (calculator-group-stats (assessment-calculator a) results))) (displayer-generate-html (assessment-displayer a) results group-stats))) #+end_src ** Helper Functions #+begin_src lisp :tangle ./src/assessment.lisp (defun string-to-keyword (str) "Converts string to keyword symbol. STR is a string. Returns keyword symbol." (intern (string-upcase str) :keyword)) #+end_src #+begin_src lisp :tangle ./src/assessment.lisp (defun string-integer (str) "Converts string to integer. STR is a string. Returns integer." (parse-integer (remove-if (complement #'digit-char-p) str))) #+end_src ** Questionnaire Responses *** Response #+name: questionnaire-response-example #+begin_src lisp (defparameter %response-example-data% '(:TYPE "likert" :NAME "visawi" :TIMESTAMP "2024-08-18 12:03:52" :POST-DATA (("group-simplicity-1-r" . "2-stimme-nicht-zu") ("group-simplicity-2" . "2-stimme-nicht-zu") ("group-simplicity-3" . "3-stimme-eher-nicht-zu") ("group-simplicity-4-r" . "6-stimme-zu") ("group-simplicity-5" . "4-neutral") ("group-diversity-1-r" . "2-stimme-nicht-zu") ("group-diversity-2" . "6-stimme-zu") ("group-diversity-3-r" . "6-stimme-zu") ("group-diversity-4" . "5-stimme-eher-zu") ("group-diversity-5" . "1-stimme-gar-nicht-zu") ("group-colorfulness-1" . "4-neutral") ("group-colorfulness-2-r" . "4-neutral") ("group-colorfulness-3-r" . "6-stimme-zu") ("group-colorfulness-4" . "2-stimme-nicht-zu") ("group-craftsmanship-1" . "5-stimme-eher-zu") ("group-craftsmanship-2-r" . "1-stimme-gar-nicht-zu") ("group-craftsmanship-3" . "2-stimme-nicht-zu") ("group-craftsmanship-4-r" . "6-stimme-zu")))) #+end_src **** Entry #+begin_src lisp (defparameter %entry-example-data% '("group-craftsmanship-4-r" . "6-stimme-zu")) #+end_src #+begin_src lisp :tangle ./src/assessment.lisp (defun response-entry-extract-group (key-str) "Extract category name from response separated by hyphens. KEY-STR is a string. Returns string." (nth 1 (uiop:split-string key-str :separator "-"))) #+end_src #+begin_src lisp :tangle ./src/assessment.lisp (defun response-entry-negative-p (key-str) "Check if entry is a negtive question. KEY-STR is a string. Returns t or nil." (let ((identifier (first (last (uiop:split-string key-str :separator "-"))))) (if (string= identifier "r") t nil))) #+end_src #+begin_src lisp :tangle ./src/assessment.lisp (defun response-entry-process (value-fn entry) "Process entry and return category and value as (:CAT (VAL)). VALUE-FN: Function which operates on the value. ENTRY: Entry from a response." (destructuring-bind (key . value) entry (let* ((category (string-to-keyword (response-entry-extract-group key))) (negative-p (response-entry-negative-p key)) (value (funcall value-fn (string-integer value) negative-p))) (list category (list value))))) #+end_src ** Interfaces The following data structure is an example which put into parse-assessment #+begin_src lisp (defparameter %categorized-responses-example% '(:VISAWI (("2024-10-10 10:42:01" ("group-simplicity-1-r" . "3-stimme-eher-nicht-zu") ("group-simplicity-2" . "5-stimme-eher-zu") ("group-simplicity-3" . "6-stimme-zu") ("group-simplicity-4-r" . "2-stimme-nicht-zu") ("group-simplicity-5" . "6-stimme-zu") ("group-diversity-1-r" . "1-stimme-gar-nicht-zu") ("group-diversity-2" . "5-stimme-eher-zu") ("group-diversity-3-r" . "5-stimme-eher-zu") ("group-diversity-4" . "1-stimme-gar-nicht-zu") ("group-diversity-5" . "6-stimme-zu") ("group-colorfulness-1" . "4-neutral") ("group-colorfulness-2-r" . "2-stimme-nicht-zu") ("group-colorfulness-3-r" . "5-stimme-eher-zu") ("group-colorfulness-4" . "5-stimme-eher-zu") ("group-craftsmanship-1" . "5-stimme-eher-zu") ("group-craftsmanship-2-r" . "3-stimme-eher-nicht-zu") ("group-craftsmanship-3" . "1-stimme-gar-nicht-zu") ("group-craftsmanship-4-r" . "4-neutral")) ("2024-10-10 10:41:34" ("group-simplicity-1-r" . "1-stimme-gar-nicht-zu") ("group-simplicity-2" . "3-stimme-eher-nicht-zu") ("group-simplicity-3" . "3-stimme-eher-nicht-zu") ("group-simplicity-4-r" . "7-stimme-voll-zu") ("group-simplicity-5" . "4-neutral") ("group-diversity-1-r" . "3-stimme-eher-nicht-zu") ("group-diversity-2" . "4-neutral") ("group-diversity-3-r" . "5-stimme-eher-zu") ("group-diversity-4" . "6-stimme-zu") ("group-diversity-5" . "2-stimme-nicht-zu") ("group-colorfulness-1" . "6-stimme-zu") ("group-colorfulness-2-r" . "2-stimme-nicht-zu") ("group-colorfulness-3-r" . "6-stimme-zu") ("group-colorfulness-4" . "3-stimme-eher-nicht-zu") ("group-craftsmanship-1" . "3-stimme-eher-nicht-zu") ("group-craftsmanship-2-r" . "1-stimme-gar-nicht-zu") ("group-craftsmanship-3" . "2-stimme-nicht-zu") ("group-craftsmanship-4-r" . "3-stimme-eher-nicht-zu"))) :SUS (("2024-07-14 21:17:21" ("group-sus-1" . "1-strongly-disagree") ("group-sus-2" . "2-disagree") ("group-sus-3" . "4-agree") ("group-sus-4" . "3-neither-agree-nor-disagree") ("group-sus-5" . "1-strongly-disagree") ("group-sus-6" . "5-strongly-agree") ("group-sus-7" . "1-strongly-disagree") ("group-sus-8" . "2-disagree") ("group-sus-9" . "4-agree") ("group-sus-10" . "3-neither-agree-nor-disagree"))))) #+end_src #+begin_src lisp :tangle ./src/assessment.lisp (defun create-assessment (type responses) (ecase type (:visawi (make-instance 'visawi-assessment :responses responses)) (:sus (make-instance 'sus-assessment :responses responses)))) #+end_src #+begin_src lisp :tangle ./src/assessment.lisp (defun parse-assessments (categorized-responses) (loop for (type data) on categorized-responses by #'cddr collect (create-assessment type data))) #+end_src ** Calculator #+begin_src lisp :tangle ./src/assessment.lisp (defclass calculator () ()) (defgeneric calculator-calc-results (calculator responses)) (defgeneric calculator-group-stats (calculator results)) #+end_src **** SUS #+begin_src lisp :tangle ./src/assessment.lisp (defun sus-recode-values (values) (let ((counter 0)) (mapcar (lambda (x) (setq counter (1+ counter)) (if (evenp counter) (- 5 x) (1- x))) values))) #+end_src #+begin_src lisp :tangle ./src/assessment.lisp (defun sus-values-with-score (values) (let ((recoded-values (sus-recode-values values))) (reverse (cons (* (apply #'+ recoded-values) 2.5) (reverse values))))) #+end_src #+begin_src lisp :tangle ./src/assessment.lisp (defclass sus-calculator (calculator) ()) (defmethod calculator-calc-results ((calc sus-calculator) responses) (loop :for response :in responses :for timestamp = (first response) :for extracted-values = (mapcar #'string-integer (mapcar #'cdr (cdr response))) :collect (cons timestamp (sus-values-with-score extracted-values)))) #+end_src #+begin_src lisp :tangle ./src/assessment.lisp (defmethod calculator-group-stats ((calc sus-calculator) results) nil) #+end_src **** VISAWI ***** Aggregate Values Per Category The following list shows an example of an response entry from the VISAWI questionnaire: #+begin_src lisp :exports code (defparameter %example-visawi-response-entry% '(("group-simplicity-1-r" . "1-stimme-gar-nicht-zu") ("group-simplicity-2" . "3-stimme-eher-nicht-zu") ("group-simplicity-3" . "3-stimme-eher-nicht-zu") ("group-simplicity-4-r" . "7-stimme-voll-zu") ("group-simplicity-5" . "4-neutral") ("group-diversity-1-r" . "3-stimme-eher-nicht-zu") ("group-diversity-2" . "4-neutral") ("group-diversity-3-r" . "5-stimme-eher-zu") ("group-diversity-4" . "6-stimme-zu") ("group-diversity-5" . "2-stimme-nicht-zu") ("group-colorfulness-1" . "6-stimme-zu") ("group-colorfulness-2-r" . "2-stimme-nicht-zu") ("group-colorfulness-3-r" . "6-stimme-zu") ("group-colorfulness-4" . "3-stimme-eher-nicht-zu") ("group-craftsmanship-1" . "3-stimme-eher-nicht-zu") ("group-craftsmanship-2-r" . "1-stimme-gar-nicht-zu") ("group-craftsmanship-3" . "2-stimme-nicht-zu") ("group-craftsmanship-4-r" . "3-stimme-eher-nicht-zu"))) #+end_src #+RESULTS: : %EXAMPLE-VISAWI-RESPONSE-ENTRY% This list of key/value entries is then processed by ~visawi-entry-process~ to extract the category and the corresponding value. For the VISAWI we need to recode the value if it is related to a negative question. #+begin_src lisp :tangle ./src/assessment.lisp (defun visawi-value-matching (value) (case value (1 7) (2 6) (3 5) (4 4) (5 3) (6 2) (7 1))) #+end_src #+begin_src lisp :tangle ./src/assessment.lisp (defun visawi-recode-value (value negative-p) "Recode response score from negative question. VALUE is a integer. NEGATIVE-P is a Predicate. Returns an integer." (if negative-p (visawi-value-matching value) value)) #+end_src #+begin_src lisp :tangle ./src/assessment.lisp (defun visawi-entry-process (entry) (response-entry-process #'visawi-recode-value entry)) #+end_src #+begin_src lisp :results value verbatim (mapcar #'visawi-entry-process %example-visawi-response-entry%) #+end_src #+RESULTS: : ((:SIMPLICITY (7)) (:SIMPLICITY (3)) (:SIMPLICITY (3)) (:SIMPLICITY (1)) : (:SIMPLICITY (4)) (:DIVERSITY (5)) (:DIVERSITY (4)) (:DIVERSITY (3)) : (:DIVERSITY (6)) (:DIVERSITY (2)) (:COLORFULNESS (6)) (:COLORFULNESS (6)) : (:COLORFULNESS (2)) (:COLORFULNESS (3)) (:CRAFTSMANSHIP (3)) : (:CRAFTSMANSHIP (7)) (:CRAFTSMANSHIP (2)) (:CRAFTSMANSHIP (5))) After processing the raw response entries we get a plist with the value as list which is import for the next step where we want to merge the values per categories. #+begin_src lisp :tangle ./src/assessment.lisp (defun merge-values-into-group (acc new-value) (let ((cat-name (first new-value)) (value (second new-value))) (setf (getf acc cat-name) (append (getf acc cat-name) value)) acc)) #+end_src #+begin_src lisp :tangle ./src/assessment.lisp (defun aggregate-values-per-group (entry-fn responses) (reduce #'merge-values-into-group (mapcar entry-fn responses) :initial-value '())) #+end_src The result is a plist with aggregated values for every category. #+begin_src lisp (defparameter %return-aggregate-values-per-group% '(:CRAFTSMANSHIP (3 7 2 5) :COLORFULNESS (6 6 2 3) :DIVERSITY (5 4 3 6 2) :SIMPLICITY (7 3 3 1 4))) #+end_src ***** Calculate Mean Score Per Category #+begin_src lisp :tangle ./src/assessment.lisp (defun mean-score-per-group (entry-fn responses) (let* ((values-per-group (aggregate-values-per-group entry-fn responses)) (mean-scores (loop for (category values) on values-per-group by #'cddr collect category collect (* 1.0 (mean values)))) (overall-mean (* 1.0 (mean (loop for (nil score) on mean-scores by #'cddr collect score))))) (append mean-scores (list :overall overall-mean)))) #+end_src ***** Calc Results #+begin_src lisp :tangle ./src/assessment.lisp (defclass visawi-calculator (calculator) ()) #+end_src #+begin_src lisp :tangle ./src/assessment.lisp (defmethod calculator-calc-results ((calc visawi-calculator) responses) (loop :for response :in responses :for timestamp = (first response) :collect (cons :timestamp (cons timestamp (mean-score-per-group #'visawi-entry-process (rest response)))))) #+end_src #+begin_src lisp (defparameter %example-calculator-calc-results% '((:TIMESTAMP "2024-10-10 10:42:01" :CRAFTSMANSHIP 3.75 :COLORFULNESS 4.5 :DIVERSITY 4.4 :SIMPLICITY 5.6 :OVERALL 4.5625) (:TIMESTAMP "2024-10-10 10:41:34" :CRAFTSMANSHIP 4.25 :COLORFULNESS 4.25 :DIVERSITY 4.0 :SIMPLICITY 3.6 :OVERALL 4.025))) #+end_src #+RESULTS: : %EXAMPLE-CALCULATOR-CALC-RESULTS% #+begin_src lisp (defmethod calculator-group-stats ((calc visawi-calculator) results) (if (> (length results) 1) (let* ((transposed-data (preprocess-and-transpose results)) (dimensions '("simplicity" "diversity" "colorfulness" "craftsmanship" "mean")) (results (mapcar #'calculate-statistics transposed-data))) (mapcar #'cons dimensions results)) nil)) #+end_src #+begin_src lisp :results none :tangle ./src/assessment.lisp (defmethod calculator-group-stats ((calc visawi-calculator) results) (if (> (length results) 1) (flet ((merge-entry (acc entry) (loop :for (key value) :on entry :by #'cddr :do (push value (getf acc key))) acc)) (let ((merged-values (reduce #'merge-entry (mapcar #'cddr results) :initial-value '()))) (reverse (loop :for (key value) :on merged-values :by #'cddr :collect (list key (calculate-statistics value)))))) nil)) #+end_src #+begin_src lisp ((:CRAFTSMANSHIP (:MEDIAN 4.0 :MEAN 4.0 :SD 0.35355338 :MIN 3.75 :MAX 4.25)) (:COLORFULNESS (:MEDIAN 4.375 :MEAN 4.375 :SD 0.17677669 :MIN 4.25 :MAX 4.5)) (:DIVERSITY (:MEDIAN 4.2 :MEAN 4.2 :SD 0.2828428 :MIN 4.0 :MAX 4.4)) (:SIMPLICITY (:MEDIAN 4.6 :MEAN 4.6 :SD 1.4142135 :MIN 3.6 :MAX 5.6)) (:OVERALL (:MEDIAN 4.29375 :MEAN 4.29375 :SD 0.38006982 :MIN 4.025 :MAX 4.5625))) #+end_src ** Displayer #+begin_src lisp :tangle ./src/assessment.lisp (defclass displayer () ()) (defgeneric displayer-generate-html (displayer results &optional group-stats)) #+end_src #+begin_src lisp :tangle ./src/assessment.lisp (defclass sus-displayer (displayer) ()) (defmethod displayer-generate-html ((disp sus-displayer) results &optional group-stats) (with-section (with-title-bar "SUS Results") (render-tbl '("Timestamp" "Q1" "Q2" "Q3" "Q4" "Q5" "Q6" "Q7" "Q8" "Q9" "Q10" "Score") results))) #+end_src #+begin_src lisp :tangle ./src/assessment.lisp (defclass visawi-displayer (displayer) ()) (defmethod displayer-generate-html ((disp visawi-displayer) results &optional group-stats) (with-section (with-title-bar "VISAWI Results") (with-section (with-title-bar "Individual Analysis") (render-kwlist-tbl results)) (when group-stats (with-section (with-title-bar "Group Analysis") (render-nested-plist-tbl group-stats))))) #+end_src ** SUS Instance #+begin_src lisp :tangle ./src/assessment.lisp (defclass sus-assessment (assessment) ()) (defmethod initialize-instance :after ((a sus-assessment) &key) (setf (assessment-calculator a) (make-instance 'sus-calculator) (assessment-displayer a) (make-instance 'sus-displayer))) #+end_src ** VISAWI Instance #+begin_src lisp :tangle ./src/assessment.lisp (defclass visawi-assessment (assessment) ()) (defmethod initialize-instance :after ((a visawi-assessment) &key) (setf (assessment-calculator a) (make-instance 'visawi-calculator) (assessment-displayer a) (make-instance 'visawi-displayer))) #+end_src