Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add initial version of learning analytics computation and dashboard illustrations #4211

Merged
merged 58 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
1b628dd
chore: rename sessions to quizzes and start working on new navbar com…
rschlaefli Aug 26, 2024
70c203f
chore(apps/analytics): add test notebook illustrating basic computati…
rschlaefli Aug 26, 2024
ed7e745
enhance: add python logic to compute daily participant analytics base…
sjschlapbach Aug 26, 2024
a1736ff
feat: add scripts to compute periodic participant learning analytics …
sjschlapbach Aug 27, 2024
b1a1b88
Merge branch 'v3' of https://github.com/uzh-bf/klicker-uzh into v3-an…
rschlaefli Aug 27, 2024
f74966e
Merge branch 'v3' into v3-analytics
rschlaefli Aug 27, 2024
97e658e
Merge branch 'v3' into v3-analytics
rschlaefli Aug 28, 2024
9a3af74
Merge branch 'v3' into v3-analytics
rschlaefli Aug 28, 2024
01eee07
Merge branch 'v3' of https://github.com/uzh-bf/klicker-uzh into v3-an…
sjschlapbach Aug 30, 2024
fe6204a
Merge branch 'v3' into v3-analytics
sjschlapbach Aug 30, 2024
7b349e7
Merge branch 'v3' of https://github.com/uzh-bf/klicker-uzh into v3-an…
rschlaefli Sep 16, 2024
fb30f90
Merge branch 'v3' into v3-analytics
sjschlapbach Sep 27, 2024
1372aa6
Merge branch 'v3' of https://github.com/uzh-bf/klicker-uzh into v3-an…
sjschlapbach Nov 11, 2024
71aeaa5
chore: improve documentation and structure of analytics service
rschlaefli Nov 11, 2024
85ad9df
chore: fix test suite after combination of extended tests with new li…
sjschlapbach Nov 11, 2024
1f81e8c
Merge branch 'v3' of https://github.com/uzh-bf/klicker-uzh into v3-an…
rschlaefli Nov 12, 2024
61754cd
Merge branch 'v3' of https://github.com/uzh-bf/klicker-uzh into v3-an…
sjschlapbach Dec 3, 2024
4ca93c7
fix(apps/analytics): ensure that free text questions without sample s…
sjschlapbach Dec 3, 2024
76c2300
enhance(apps/analytics): add scripts for the computation of aggregate…
sjschlapbach Dec 4, 2024
e8f2791
fix(apps/analytics): ensure that correct response data is queried for…
sjschlapbach Dec 4, 2024
4b60b7e
enhance(apps/analytics): add logic for computation of participant cou…
sjschlapbach Dec 4, 2024
4e11fa1
enhance(apps/analytics): add logic for the computation of aggregated …
sjschlapbach Dec 4, 2024
5f402d7
chore(apps/analytics): add database tables for learning analytics pro…
sjschlapbach Dec 5, 2024
cb471c3
enhance(apps/analytics): add computation logic for participant course…
sjschlapbach Dec 5, 2024
a04be0a
enhance(apps/analytics): add computation logic for instance and activ…
sjschlapbach Dec 6, 2024
28d64f6
enhance(apps/analytics): add computation logic for activity progress …
sjschlapbach Dec 6, 2024
b8a134a
enhance: add first version of activity dashboard with daily and weekl…
sjschlapbach Dec 6, 2024
a799972
enhance(apps/analytics): add course data comparison for weekly studen…
sjschlapbach Dec 7, 2024
189cfed
enhance(apps/frontend-manage): add loading state for comparison cours…
sjschlapbach Dec 9, 2024
69cc23b
enhance(apps/analytics): add student activity to learning analytics a…
sjschlapbach Dec 12, 2024
1310b0c
enhance(apps/analytics): add illustrations for asynchronous activity …
sjschlapbach Dec 13, 2024
dd3014f
enhance(apps/analytics): add illustrations for performance rates on a…
sjschlapbach Dec 18, 2024
5630619
enhance(apps/analytics): add illustration of individual student perfo…
sjschlapbach Dec 19, 2024
990cd6f
enhance(apps/analytics): add overview of element feedbacks on activit…
sjschlapbach Dec 20, 2024
a1d190c
Merge branch 'v3' of https://github.com/uzh-bf/klicker-uzh into v3-an…
sjschlapbach Dec 20, 2024
4f6a3d0
Merge branch 'v3' of https://github.com/uzh-bf/klicker-uzh into v3-an…
rschlaefli Dec 20, 2024
c5f02ce
chore: add pnpm install --frozen-lockfile to pre-commit hook
rschlaefli Dec 20, 2024
09b14b1
style(apps/analytics): organize performance analytics in tab structur…
sjschlapbach Dec 20, 2024
1a7b2eb
deps: upgrade to prisma 6.1.0 (#4403)
rschlaefli Dec 20, 2024
6fa3bb6
enhance(apps/analytics): implement quiz analytics dashboard for async…
sjschlapbach Dec 22, 2024
4a6f88e
enhance(apps/analytics): allow switching between courses and activiti…
sjschlapbach Dec 22, 2024
2041e65
chore(apps/analytics): make sure that analytics pages do not break fo…
sjschlapbach Dec 22, 2024
c8e79fc
enhance(apps/frontend-manage): add links from and to learning analyti…
sjschlapbach Dec 22, 2024
1e721db
fix(apps/frontend-manage): ensure that activity evaluation sidebar re…
sjschlapbach Dec 22, 2024
9817d84
Merge branch 'v3' into v3-analytics
rschlaefli Dec 22, 2024
cbfd255
chore: remove duplicate prisma v6 migration
rschlaefli Dec 22, 2024
62f66b3
Merge branch 'v3' into v3-analytics
sjschlapbach Dec 22, 2024
82577c0
Merge branch 'v3' of https://github.com/uzh-bf/klicker-uzh into v3-an…
sjschlapbach Dec 22, 2024
21be1e8
Merge branch 'v3' into v3-analytics
sjschlapbach Dec 22, 2024
a47b11f
Merge branch 'v3' into v3-analytics
sjschlapbach Dec 22, 2024
00be5b1
Merge branch 'v3' into v3-analytics
sjschlapbach Dec 22, 2024
ebbb581
enhance(apps/analytics): add computation logic for participant activi…
sjschlapbach Dec 23, 2024
593cef6
chore(apps/analytics): create executable python scripts for initial l…
sjschlapbach Dec 23, 2024
08acc27
Merge branch 'v3' of https://github.com/uzh-bf/klicker-uzh into v3-an…
rschlaefli Dec 23, 2024
c4b22e0
fix(cypress): resolve translation issue in microlearning test workflo…
sjschlapbach Dec 23, 2024
30c5afd
chore: update app version in learning analytics package file
sjschlapbach Dec 23, 2024
645930c
chore: add shell scripts to execute all learning analytics initializa…
sjschlapbach Dec 23, 2024
f8c8cb1
enhance(apps/analytics): add illustration of participant activity per…
sjschlapbach Dec 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ packages/prisma/src/seed
.turbo

out/
!out/.gitkeep
.rollup.cache/
24 changes: 24 additions & 0 deletions apps/analytics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# KlickerUZH Analytics

This service computes learning analytics for KlickerUZH, providing insights into student learning patterns and performance metrics.

## Requirements

- Python 3.12.x (e.g., installed through `asdf`)
- Node.js 20.x.x
- Poetry

## Setup

- The project uses Poetry for dependency management and environment isolation. Make sure you have Poetry installed before proceeding. Then run `poetry install` in this folder to prepare the virtual environment.
- The project uses PNPM to simplify the execution of scripts and to provide a watch mode for execution. Make sure that you have executed `pnpm install` in the repository before trying to run the commands below.
- Make sure that all `.prisma` files are available in `prisma/`. If this is not the case, run the `util/sync-schema.sh` script first.
- Make sure that a valid Python environment is used (3.12). If poetry tries to use an environment not matching specifications, the install command or script execution might fail. The Python binary to be used can be set expliticly using `poetry env use /Users/.../bin/python` (after which `poetry install` has to be run). Tools like `asdf` allow the clean management of multiple Python versions on a single machine.

## Available Commands

The following commands are available through PNPM:

- `pnpm generate` - Generate the Prisma client for database access in Python
- `pnpm main` - Run the analytics service
- `pnpm dev` - Start the service in watch mode for development
13 changes: 13 additions & 0 deletions apps/analytics/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@klicker-uzh/analytics",
"version": "3.3.0-alpha.8",
"license": "AGPL-3.0",
"devDependencies": {
"nodemon": "~3.1.7"
},
"scripts": {
"dev": "doppler run --config dev -- nodemon --exec 'poetry run poe main' --watch src,prisma --ext py,prisma",
"generate": "poetry run poe generate",
"main": "doppler run --config dev -- poetry run poe main"
}
}
1,053 changes: 548 additions & 505 deletions apps/analytics/poetry.lock

Large diffs are not rendered by default.

16 changes: 11 additions & 5 deletions apps/analytics/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,31 @@
name = "@klicker-uzh/analytics"
version = "0.0.1"
description = ""
authors = ["Roland Schlaefli <roland.schlaefli@df.uzh.ch>"]
authors = ["Roland Schlaefli <roland.schlaefli@df.uzh.ch>", "Julius Schlapbach <julius.schlapbach@df.uzh.ch>"]
license = "AGPL-3.0"
readme = "README.md"
packages = [{include = "@klicker_uzh"}]
package-mode = false

[tool.poetry.dependencies]
python = "^3.12"
pandas = "2.2.2"
prisma = "0.14.0"
xlsxwriter = "^3.2.0"
prisma = "0.15.0"
xlsxwriter = "3.2.0"

[tool.poetry.dev-dependencies]
poethepoet = "0.27.0"
ipykernel = "6.29.5"

[tool.poetry.group.dev.dependencies]
pyright = "1.1.376"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.poe.tasks]
generate = "prisma generate"
main = "doppler run --config dev -- python main.py"
main = "doppler run --config dev -- python -m src.main"

[tool.pyright]
typeCheckingMode = "strict"
2 changes: 2 additions & 0 deletions apps/analytics/src/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .modules import *
from .notebooks import *
File renamed without changes.
7 changes: 7 additions & 0 deletions apps/analytics/src/modules/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .participant_analytics import *
from .aggregated_analytics import *
from .participant_course_analytics import *
from .aggregated_course_analytics import *
from .participant_performance import *
from .instance_activity_performance import *
from .activity_progress import *
4 changes: 4 additions & 0 deletions apps/analytics/src/modules/activity_progress/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .get_course_progress_activities import get_course_progress_activities
from .compute_progress_counts import compute_progress_counts
from .save_practice_quiz_progress import save_practice_quiz_progress
from .save_microlearning_progress import save_microlearning_progress
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import pandas as pd


def compute_progress_counts(activity):
started_count = 0
completed_count = 0
repeated_count = 0

if len(activity["responses"]) != 0:
# count number of elements in activity stacks
num_elements = 0
for stack in activity["stacks"]:
num_elements += len(stack["elements"])

# group the activity responses by participant and count them
df_responses = pd.DataFrame(activity["responses"])
df_statistics = (
df_responses[["id", "trialsCount", "participantId"]]
.groupby("participantId")
.agg({"id": "count", "trialsCount": "min"})
.rename(columns={"id": "count", "trialsCount": "min_trials"})
)

# compute number of participants that have started the activity
started_count = len(df_statistics[df_statistics["count"] <= num_elements])

# compute number of participants that have completed the activity
completed_count = len(df_statistics[df_statistics["count"] == num_elements])

# count the number of participants that have repeated the activity (completed and min_trials >= 2)
repeated_count = len(
df_statistics[
(df_statistics["count"] == num_elements)
& (df_statistics["min_trials"] >= 2)
]
)

else:
print("No responses found for activity", activity["id"])

return started_count, completed_count, repeated_count
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
def get_course_progress_activities(db, course_id):
pqs = db.practicequiz.find_many(
where={"courseId": course_id},
include={"stacks": {"include": {"elements": True}}, "responses": True},
)
pqs = list(map(lambda x: x.dict(), pqs))

mls = db.microlearning.find_many(
where={"courseId": course_id},
include={"stacks": {"include": {"elements": True}}, "responses": True},
)
mls = list(map(lambda x: x.dict(), mls))

return pqs, mls
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
def save_microlearning_progress(
db,
course_participants,
started_count,
completed_count,
course_id,
ml_id,
):
values = {
"totalCourseParticipants": course_participants,
"startedCount": started_count,
"completedCount": completed_count,
}
creation_values = values.copy()
creation_values["course"] = {"connect": {"id": course_id}}
creation_values["microLearning"] = {"connect": {"id": ml_id}}

db.activityprogress.upsert(
where={"microLearningId": ml_id},
data={"create": creation_values, "update": values},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
def save_practice_quiz_progress(
db,
course_participants,
started_count,
completed_count,
repeated_count,
course_id,
quiz_id,
):
values = {
"totalCourseParticipants": course_participants,
"startedCount": started_count,
"completedCount": completed_count,
"repeatedCount": repeated_count,
}
creation_values = values.copy()
creation_values["course"] = {"connect": {"id": course_id}}
creation_values["practiceQuiz"] = {"connect": {"id": quiz_id}}

db.activityprogress.upsert(
where={"practiceQuizId": quiz_id},
data={"create": creation_values, "update": values},
)
4 changes: 4 additions & 0 deletions apps/analytics/src/modules/aggregated_analytics/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .compute_aggregated_analytics import compute_aggregated_analytics
from .load_participant_analytics import load_participant_analytics
from .aggregate_participant_analytics import aggregate_participant_analytics
from .save_aggregated_analytics import save_aggregated_analytics
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
def aggregate_participant_analytics(df_participant_analytics, verbose=False):
# if the dataframe is empty, return None
if df_participant_analytics.empty:
if verbose:
print("No participant analytics to aggregate")

return None

# aggreagte all participant analytics for the specified time range and separate courses
df_aggregated_analytics = (
df_participant_analytics.groupby("courseId")
.agg(
{
"id": "count",
"responseCount": "sum",
"totalScore": "sum",
"totalPoints": "sum",
"totalXp": "sum",
}
)
.reset_index()
.rename(
columns={
"id": "participantCount",
}
)
)

return df_aggregated_analytics
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from .load_participant_analytics import load_participant_analytics
from .aggregate_participant_analytics import aggregate_participant_analytics
from .save_aggregated_analytics import save_aggregated_analytics


def compute_aggregated_analytics(
db, start_date, end_date, timestamp, analytics_type="DAILY", verbose=False
):
# load all participant analytics for the given timestamp and analytics time range
df_participant_analytics = load_participant_analytics(
db, timestamp, analytics_type, verbose
)

# aggregate all participant analytics values by course
df_aggregated_analytics = aggregate_participant_analytics(
df_participant_analytics, verbose
)

if df_aggregated_analytics is not None and verbose:
print("Aggregated analytics for time range:" + start_date + " to " + end_date)
print(df_aggregated_analytics.head())
elif df_aggregated_analytics is None:
print(
"No aggregated analytics to compute for time range:"
+ start_date
+ " to "
+ end_date
)

# store the computed aggregated analytics in the database
if df_aggregated_analytics is not None:
save_aggregated_analytics(
db, df_aggregated_analytics, timestamp, analytics_type
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import pandas as pd


def convert_to_df(analytics):
# convert the database query result into a pandas dataframe
rows = []
for item in analytics:
rows.append(dict(item))

return pd.DataFrame(rows)


def load_participant_analytics(db, timestamp, analytics_type, verbose=False):
participant_analytics = db.participantanalytics.find_many(
where={"timestamp": timestamp, "type": analytics_type},
)

if verbose:
# Print the first participant analytics
print(
"Found {} analytics for the timespan from {} to {}".format(
len(participant_analytics), start_date, end_date
)
)
print(participant_analytics[0])

# convert the analytics to a dataframe
df_loaded_analytics = convert_to_df(participant_analytics)

return df_loaded_analytics
Loading
Loading