871 lines
31 KiB
Org Mode
871 lines
31 KiB
Org Mode
|
#+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
|
||
|
<!DOCTYPE html>
|
||
|
<html lang=en data-bs-theme=light>
|
||
|
<head>
|
||
|
<meta charset=utf-8>
|
||
|
<meta name=viewport content="width=device-width, initial-scale=1">
|
||
|
<meta http-equiv=Content-Security-Policy
|
||
|
content=upgrade-insecure-requests>
|
||
|
<title>Survey Details</title>
|
||
|
<style>@media print{.navbar,.nav,.btn,.carousel,.dropdown-menu,footer,.no-print{display:none!important}.container,.container-fluid{width:100%!important;padding:0!important;margin:0!important}[class^="col-"]{width:100%!important;flex:0 0 100%!important;max-width:100%!important}body{font-size:12pt;line-height:1.5}h1,.h1{font-size:24pt}h2,.h2{font-size:22pt}h3,.h3{font-size:20pt}h4,.h4{font-size:18pt}h5,.h5{font-size:16pt}h6,.h6{font-size:14pt}h1,h2,h3,h4,h5,h6,p,img,table,figure{page-break-inside:avoid}a{text-decoration:underline;color:#000!important}a[href^="http"]:after{content:" (" attr(href) ")";font-size:80%}.table-responsive{overflow:visible!important}.card{border:none!important;box-shadow:none!important}@page{margin:1in}}</style>
|
||
|
<link rel=stylesheet type="text/css"
|
||
|
href="/5.3.2/bootstrap.min.css">
|
||
|
<script>(()=>{'use strict';const getStoredTheme=()=>localStorage.getItem('theme');const getPreferredTheme=()=>{const storedTheme=getStoredTheme();if(storedTheme){return storedTheme}return window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'};const setTheme=theme=>{if(theme==='auto'){document.documentElement.setAttribute('data-bs-theme',(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'))}else{document.documentElement.setAttribute('data-bs-theme',theme)}};setTheme(getPreferredTheme())})();</script>
|
||
|
</head>
|
||
|
<body>
|
||
|
<header class=mb-3>
|
||
|
<div class="skippy visually-hidden-focusable overflow-hidden">
|
||
|
<a class="d-inline-flex p-2 m-1" href=#main-content>Skip to main content</a>
|
||
|
</div>
|
||
|
<nav class="navbar navbar-expand-lg bg-body-tertiary mb-5"
|
||
|
aria-label=Main>
|
||
|
<div class=container>
|
||
|
<a class=navbar-brand href="/">ml-survey</a>
|
||
|
<button class=navbar-toggler type=button data-bs-toggle=collapse
|
||
|
data-bs-target=#navbarNav aria-controls=navbarNav
|
||
|
aria-expanded=false aria-label="Toggle navigation"><span class=navbar-toggler-icon></span></button>
|
||
|
<div class="collapse navbar-collapse" id=navbarNav>
|
||
|
<ul class=navbar-nav>
|
||
|
<li class=nav-item><a class=nav-link href="/">Home</a>
|
||
|
<li class=nav-item><a class="nav-link active" aria-current=page href="/new-survey">New Survey</a>
|
||
|
</ul>
|
||
|
</div>
|
||
|
</div>
|
||
|
</nav>
|
||
|
<div class=container>
|
||
|
<h1>Survey Details</h1>
|
||
|
</div>
|
||
|
</header>
|
||
|
<main class=container id=main-content>
|
||
|
<section class=mb-3>
|
||
|
<div
|
||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||
|
<h2>Properties</h2>
|
||
|
</div>
|
||
|
<p>ID: 1057733929973421737730
|
||
|
<dl>
|
||
|
<dt>title
|
||
|
<dd>dasdas
|
||
|
<dt>description
|
||
|
<dd>fsdfsdfs
|
||
|
<dt>questionnaire
|
||
|
<dd><a href="/survey/1057733929973421737730/de/sus">Open Questionnaire /de/sus</a>
|
||
|
<dt>questionnaire
|
||
|
<dd><a href="/survey/1057733929973421737730/de/visawi">Open Questionnaire /de/visawi</a>
|
||
|
</dl>
|
||
|
</section>
|
||
|
<section class=mb-3>
|
||
|
<div
|
||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||
|
<h2>Assesments</h2>
|
||
|
</div>
|
||
|
<section class=mb-3>
|
||
|
<div
|
||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||
|
<h3>VISAWI Results</h3>
|
||
|
</div>
|
||
|
<section class=mb-3>
|
||
|
<div
|
||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||
|
<h4>Individual Analysis</h4>
|
||
|
</div>
|
||
|
<table class=table>
|
||
|
<thead>
|
||
|
<tr>
|
||
|
<th scope=col>TIMESTAMP
|
||
|
<th scope=col>CRAFTSMANSHIP
|
||
|
<th scope=col>COLORFULNESS
|
||
|
<th scope=col>DIVERSITY
|
||
|
<th scope=col>SIMPLICITY
|
||
|
<th scope=col>OVERALL
|
||
|
|
||
|
<tbody>
|
||
|
<tr>
|
||
|
<td>2024-10-10 10:41:34
|
||
|
<td>4.25
|
||
|
<td>4.25
|
||
|
<td>4.00
|
||
|
<td>3.60
|
||
|
<td>4.03
|
||
|
<tr>
|
||
|
<td>2024-10-10 10:42:01
|
||
|
<td>3.75
|
||
|
<td>4.50
|
||
|
<td>4.40
|
||
|
<td>5.60
|
||
|
<td>4.56
|
||
|
|
||
|
|
||
|
</table>
|
||
|
</section>
|
||
|
<section class=mb-3>
|
||
|
<div
|
||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||
|
<h4>Group Analysis</h4>
|
||
|
</div>
|
||
|
<table class=table>
|
||
|
<thead>
|
||
|
<tr>
|
||
|
<th scope=col>NAME
|
||
|
<th scope=col>MEDIAN
|
||
|
<th scope=col>MEAN
|
||
|
<th scope=col>SD
|
||
|
<th scope=col>MIN
|
||
|
<th scope=col>MAX
|
||
|
|
||
|
<tbody>
|
||
|
<tr>
|
||
|
<td>craftsmanship
|
||
|
<td>4.00
|
||
|
<td>4.00
|
||
|
<td>0.35
|
||
|
<td>3.75
|
||
|
<td>4.25
|
||
|
<tr>
|
||
|
<td>colorfulness
|
||
|
<td>4.38
|
||
|
<td>4.38
|
||
|
<td>0.18
|
||
|
<td>4.25
|
||
|
<td>4.50
|
||
|
<tr>
|
||
|
<td>diversity
|
||
|
<td>4.20
|
||
|
<td>4.20
|
||
|
<td>0.28
|
||
|
<td>4.00
|
||
|
<td>4.40
|
||
|
<tr>
|
||
|
<td>simplicity
|
||
|
<td>4.60
|
||
|
<td>4.60
|
||
|
<td>1.41
|
||
|
<td>3.60
|
||
|
<td>5.60
|
||
|
<tr>
|
||
|
<td>overall
|
||
|
<td>4.29
|
||
|
<td>4.29
|
||
|
<td>0.38
|
||
|
<td>4.03
|
||
|
<td>4.56
|
||
|
|
||
|
|
||
|
</table>
|
||
|
</section>
|
||
|
</section>
|
||
|
<section class=mb-3>
|
||
|
<div
|
||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||
|
<h3>SUS Results</h3>
|
||
|
</div>
|
||
|
<table class=table>
|
||
|
<thead>
|
||
|
<tr>
|
||
|
<th scope=col>Timestamp
|
||
|
<th scope=col>Q1
|
||
|
<th scope=col>Q2
|
||
|
<th scope=col>Q3
|
||
|
<th scope=col>Q4
|
||
|
<th scope=col>Q5
|
||
|
<th scope=col>Q6
|
||
|
<th scope=col>Q7
|
||
|
<th scope=col>Q8
|
||
|
<th scope=col>Q9
|
||
|
<th scope=col>Q10
|
||
|
<th scope=col>Score
|
||
|
|
||
|
<tbody>
|
||
|
<tr>
|
||
|
<td>2024-07-14 21:17:21
|
||
|
<td>1.00
|
||
|
<td>2.00
|
||
|
<td>4.00
|
||
|
<td>3.00
|
||
|
<td>1.00
|
||
|
<td>5.00
|
||
|
<td>1.00
|
||
|
<td>2.00
|
||
|
<td>4.00
|
||
|
<td>3.00
|
||
|
<td>40.00
|
||
|
|
||
|
|
||
|
</table>
|
||
|
</section>
|
||
|
</section>
|
||
|
</main>
|
||
|
</body>
|
||
|
<script src="/5.3.2/bootstrap.bundle.min.js"></script>
|
||
|
</html>
|
||
|
#+end_export
|
||
|
|
||
|
#+begin_src http :pretty :exports code :results html
|
||
|
GET http://localhost:8080/survey/252366393230629784388
|
||
|
#+end_src
|
||
|
|
||
|
#+RESULTS:
|
||
|
#+begin_export html
|
||
|
<!DOCTYPE html>
|
||
|
<html lang=en data-bs-theme=light>
|
||
|
<head>
|
||
|
<meta charset=utf-8>
|
||
|
<meta name=viewport content="width=device-width, initial-scale=1">
|
||
|
<meta http-equiv=Content-Security-Policy
|
||
|
content=upgrade-insecure-requests>
|
||
|
<title>Survey Details</title>
|
||
|
<style>@media print{.navbar,.nav,.btn,.carousel,.dropdown-menu,footer,.no-print{display:none!important}.container,.container-fluid{width:100%!important;padding:0!important;margin:0!important}[class^="col-"]{width:100%!important;flex:0 0 100%!important;max-width:100%!important}body{font-size:12pt;line-height:1.5}h1,.h1{font-size:24pt}h2,.h2{font-size:22pt}h3,.h3{font-size:20pt}h4,.h4{font-size:18pt}h5,.h5{font-size:16pt}h6,.h6{font-size:14pt}h1,h2,h3,h4,h5,h6,p,img,table,figure{page-break-inside:avoid}a{text-decoration:underline;color:#000!important}a[href^="http"]:after{content:" (" attr(href) ")";font-size:80%}.table-responsive{overflow:visible!important}.card{border:none!important;box-shadow:none!important}@page{margin:1in}}</style>
|
||
|
<link rel=stylesheet type="text/css"
|
||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
||
|
<script>(()=>{'use strict';const getStoredTheme=()=>localStorage.getItem('theme');const getPreferredTheme=()=>{const storedTheme=getStoredTheme();if(storedTheme){return storedTheme}return window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'};const setTheme=theme=>{if(theme==='auto'){document.documentElement.setAttribute('data-bs-theme',(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'))}else{document.documentElement.setAttribute('data-bs-theme',theme)}};setTheme(getPreferredTheme())})();</script>
|
||
|
</head>
|
||
|
<body>
|
||
|
<header class=mb-3>
|
||
|
<div class="skippy visually-hidden-focusable overflow-hidden">
|
||
|
<a class="d-inline-flex p-2 m-1" href=#main-content>Skip to main content</a>
|
||
|
</div>
|
||
|
<nav class="navbar navbar-expand-lg bg-body-tertiary mb-5"
|
||
|
aria-label=Main>
|
||
|
<div class=container>
|
||
|
<a class=navbar-brand href="/">ml-survey</a>
|
||
|
<button class=navbar-toggler type=button data-bs-toggle=collapse
|
||
|
data-bs-target=#navbarNav aria-controls=navbarNav
|
||
|
aria-expanded=false aria-label="Toggle navigation"><span class=navbar-toggler-icon></span></button>
|
||
|
<div class="collapse navbar-collapse" id=navbarNav>
|
||
|
<ul class=navbar-nav>
|
||
|
<li class=nav-item><a class=nav-link href="/">Home</a>
|
||
|
<li class=nav-item><a class="nav-link active" aria-current=page href="/new-survey">New Survey</a>
|
||
|
</ul>
|
||
|
</div>
|
||
|
</div>
|
||
|
</nav>
|
||
|
<div class=container>
|
||
|
<h1>Survey Details</h1>
|
||
|
</div>
|
||
|
</header>
|
||
|
<main class=container id=main-content>
|
||
|
<section class=mb-3>
|
||
|
<div
|
||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||
|
<h2>Properties</h2>
|
||
|
</div>
|
||
|
<p>ID: 252366393230629784388
|
||
|
<dl>
|
||
|
<dt>title
|
||
|
<dd>test2
|
||
|
<dt>description
|
||
|
<dd>test2
|
||
|
<dt>questionnaire
|
||
|
<dd><a href="/survey/252366393230629784388/de/sus">Open Questionnaire /de/sus</a>
|
||
|
<dt>questionnaire
|
||
|
<dd><a href="/survey/252366393230629784388/de/visawi">Open Questionnaire /de/visawi</a>
|
||
|
</dl>
|
||
|
</section>
|
||
|
<section class=mb-3>
|
||
|
<div
|
||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||
|
<h2>Assesments</h2>
|
||
|
</div>
|
||
|
<section class=mb-3>
|
||
|
<div
|
||
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||
|
<h3>SUS Results</h3>
|
||
|
</div>
|
||
|
<table class=table>
|
||
|
<thead>
|
||
|
<tr>
|
||
|
<th scope=col>Timestamp
|
||
|
<th scope=col>Q1
|
||
|
<th scope=col>Q2
|
||
|
<th scope=col>Q3
|
||
|
<th scope=col>Q4
|
||
|
<th scope=col>Q5
|
||
|
<th scope=col>Q6
|
||
|
<th scope=col>Q7
|
||
|
<th scope=col>Q8
|
||
|
<th scope=col>Q9
|
||
|
<th scope=col>Q10
|
||
|
<th scope=col>Score
|
||
|
|
||
|
<tbody>
|
||
|
<tr>
|
||
|
<td>2024-10-10 18:15:27
|
||
|
<td>3.00
|
||
|
<td>2.00
|
||
|
<td>3.00
|
||
|
<td>4.00
|
||
|
<td>4.00
|
||
|
<td>4.00
|
||
|
<td>1.00
|
||
|
<td>2.00
|
||
|
<td>4.00
|
||
|
<td>3.00
|
||
|
<td>50.00
|
||
|
|
||
|
|
||
|
</table>
|
||
|
</section>
|
||
|
</section>
|
||
|
</main>
|
||
|
</body>
|
||
|
<script
|
||
|
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||
|
</html>
|
||
|
#+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-category (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-category 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-category (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-category (entry-fn responses)
|
||
|
(reduce #'merge-values-into-category
|
||
|
(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-category%
|
||
|
'(: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-category (entry-fn responses)
|
||
|
(let* ((values-per-category (aggregate-values-per-category entry-fn responses))
|
||
|
(mean-scores (loop for (category values) on values-per-category 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-category #'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
|