Skip to content

Commit

Permalink
work on tests for score calculation for groups
Browse files Browse the repository at this point in the history
  • Loading branch information
MaHaWo committed Dec 4, 2024
1 parent 9cca572 commit 7b948a5
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 68 deletions.
3 changes: 2 additions & 1 deletion mondey_backend/src/mondey_backend/models/milestones.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ class MilestoneAnswerSessionPublic(SQLModel):
answers: dict[int, MilestoneAnswerPublic]


# models for statistics
# models for statistics. README: Perhaps this could be made simpler if the data was stored in a database with array-column support. sqlite apparently doesnt: https://stackoverflow.com/questions/3005231/how-to-store-array-in-one-column-in-sqlite3, but postgres does: https://www.postgresql.org/docs/9.1/arrays.html
# will be returned to later. Issue no.
class MilestoneAgeScore(SQLModel, table=True):
milestone_id: int | None = Field(
default=None,
Expand Down
36 changes: 26 additions & 10 deletions mondey_backend/src/mondey_backend/routers/scores.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,31 +88,45 @@ def compute_milestonegroup_feedback_summary(
session: SessionDep, child_id: int, answersession_id: int
) -> dict[int, int]:
answersession = session.get(MilestoneAnswerSession, answersession_id)
print("answersession in stat: ", answersession)
# get child age
child = session.get(Child, child_id)
age = get_child_age_in_months(child, answersession.created_at)

# extract milestonegroups
groups = set(answer.milestone_group_id for answer in answersession.answers.values())
today = datetime.today()
today = datetime.now()

# for each milestonegroup, get the statistics, compute the current mean, and compute the feedback
feedback: dict[int, int] = {}
print("age: ", age)
for group in groups:
print(" current group ", group, "of: ", groups)
# try to get statistics to use as evaluation basis and recompute if it's not there
# or is too old
stats = session.exec(
select(MilestoneGroupAgeScoreCollection).where(
MilestoneGroupAgeScoreCollection.milestonegroup_id == group
MilestoneGroupAgeScoreCollection.milestone_group_id == group
)
).first()
print("stats before ", stats, stats.scores[age])
if stats is None or stats.created_at < today - timedelta(days=7):
stats = calculate_milestonegroup_statistics_by_age(session, group)
session.add(stats)

print("stats after ", stats, stats.scores[age])
print(" stats before ", stats)
print(stats.scores[age])
print(
" conditions: ", stats is None, stats.created_at < today - timedelta(days=7)
)
print(" created at: ", stats.created_at)
if stats is None or stats.created_at < today - timedelta(days=7):
print("recomputing stats")
new_stats = calculate_milestonegroup_statistics_by_age(session, group)
# update stuff in database
for new_score in new_stats.scores:
print(new_score)
session.merge(new_score)
session.merge(new_stats)
session.commit()
stats = new_stats
print(" stats after ", stats)

# extract the answers for the current milestone group
group_answers = [
Expand All @@ -122,15 +136,17 @@ def compute_milestonegroup_feedback_summary(
]

print(
"group answers: ", group_answers, np.mean(group_answers), min(group_answers)
" group answers: ",
group_answers,
np.mean(group_answers),
min(group_answers),
)

# use the statistics recorded for a certain age as the basis for the feedback computation
feedback[group] = compute_feedback_simple(
stats.scores[age], np.mean(group_answers), min(group_answers)
)

session.commit()
print("feedback: ", feedback)
return feedback


Expand Down
113 changes: 70 additions & 43 deletions mondey_backend/src/mondey_backend/routers/statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def calculate_milestone_statistics_by_age(
MilestoneAnswer.milestone_id == milestone_id
)
else:
# fetch all answers that have been added in answersessions after the last statistics were calculated
answers_query = (
select(MilestoneAnswer)
.join(
Expand All @@ -130,29 +131,31 @@ def calculate_milestone_statistics_by_age(
)
)
answers = session.exec(answers_query).all()

count, avg_scores, stddev_scores = _get_statistics_by_age(
answers, child_ages, count=count, avg=avg_scores, stddev=stddev_scores
)
expected_age = _get_expected_age_from_scores(avg_scores)

# overwrite last_statistics with updated stuff
return MilestoneAgeScoreCollection(
milestone_id=milestone_id,
expected_age=expected_age,
created_at=datetime.datetime.now(),
scores=[
MilestoneAgeScore(
age=age,
milestone_id=milestone_id,
count=count[age],
avg_score=avg_scores[age],
stddev_score=stddev_scores[age],
expected_score=4 if age >= expected_age else 1,
)
for age in range(0, len(avg_scores))
],
)
if len(answers) == 0:
return last_statistics
else:
count, avg_scores, stddev_scores = _get_statistics_by_age(
answers, child_ages, count=count, avg=avg_scores, stddev=stddev_scores
)
expected_age = _get_expected_age_from_scores(avg_scores)

# overwrite last_statistics with updated stuff
return MilestoneAgeScoreCollection(
milestone_id=milestone_id,
expected_age=expected_age,
created_at=datetime.datetime.now(),
scores=[
MilestoneAgeScore(
age=age,
milestone_id=milestone_id,
count=count[age],
avg_score=avg_scores[age],
stddev_score=stddev_scores[age],
expected_score=4 if age >= expected_age else 1,
)
for age in range(0, len(avg_scores))
],
)


def calculate_milestonegroup_statistics_by_age(
Expand All @@ -173,8 +176,12 @@ def calculate_milestonegroup_statistics_by_age(
avg_scores = None
stddev_scores = None
if last_statistics is not None:
count = np.array([score.count for score in last_statistics.scores])
avg_scores = np.array([score.avg_score for score in last_statistics.scores])
count = np.array(
[score.count for score in last_statistics.scores], dtype=np.int32
)
avg_scores = np.array(
[score.avg_score for score in last_statistics.scores], dtype=np.float64
)
stddev_scores = np.array(
[score.stddev_score for score in last_statistics.scores]
)
Expand All @@ -186,6 +193,7 @@ def calculate_milestonegroup_statistics_by_age(
col(MilestoneAnswer.milestone_group_id) == milestonegroup_id
)
else:
# fetch all answers that have been added in answersessions after the last statistics were calculated
answer_query = (
select(MilestoneAnswer)
.join(
Expand All @@ -199,21 +207,40 @@ def calculate_milestonegroup_statistics_by_age(
)

answers = session.exec(answer_query).all()

count, avg, stddev = _get_statistics_by_age(
answers, child_ages, count=count, avg=avg_scores, stddev=stddev_scores
)
return MilestoneGroupAgeScoreCollection(
milestone_group_id=milestonegroup_id,
scores=[
MilestoneGroupAgeScore(
milestone_group_id=milestonegroup_id,
age=age,
count=count[age],
avg_score=avg[age],
stddev_score=stddev[age],
)
for age in range(0, len(avg))
],
created_at=datetime.datetime.now(),
)
if len(answers) == 0:
return last_statistics
else:
print("answers for group statistics: ", answers)
print(
"old statistics: ",
avg_scores[avg_scores > 0],
stddev_scores[stddev_scores > 0],
)
count, avg, stddev = _get_statistics_by_age(
answers, child_ages, count=count, avg=avg_scores, stddev=stddev_scores
)
print(
"new statistics: ",
np.nonzero(count),
np.nonzero(avg),
np.nonzero(stddev),
count[count > 0],
avg[avg > 0],
stddev[stddev > 0],
)
return MilestoneGroupAgeScoreCollection(
milestone_group_id=milestonegroup_id,
scores=[
MilestoneGroupAgeScore(
milestone_group_id=milestonegroup_id,
age=age,
count=int(
count[age]
), # need a conversion to avoid numpy.int32 being stored as byte object
avg_score=avg[age],
stddev_score=stddev[age],
)
for age in range(0, len(avg))
],
created_at=datetime.datetime.now(),
)
28 changes: 23 additions & 5 deletions mondey_backend/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ def session(children: list[dict]):
answer_session_id=2, milestone_id=2, milestone_group_id=1, answer=2
)
)

# add an (expired) milestone answer session for child 3 / admin user (id 1) with 1 answer
session.add(
MilestoneAnswerSession(
Expand Down Expand Up @@ -353,26 +354,43 @@ def session(children: list[dict]):
created_at=datetime.datetime.today() - datetime.timedelta(days=1),
)
)

session.add(
MilestoneAgeScoreCollection(
milestone_id=7,
expected_age=42,
created_at=datetime.datetime.today() - datetime.timedelta(days=1),
created_at=datetime.datetime.today() - datetime.timedelta(days=12),
)
)

# add basic statistics for milestonegroup 1
# add basic statistics for milestonegroups, one that is current and one that is outdated
session.add(
MilestoneGroupAgeScoreCollection(
milestonegroup_id=1,
created_at=datetime.datetime.today() - datetime.timedelta(days=1),
)
)

session.add(
MilestoneGroupAgeScoreCollection(
milestonegroup_id=2,
created_at=datetime.datetime.today() - datetime.timedelta(days=1),
created_at=datetime.datetime.today() - datetime.timedelta(days=12),
)
)

# add an expired milestone answer session that is newer than the last statistics for milestonegroup
session.add(
MilestoneAnswerSession(
child_id=3, user_id=1, created_at=today - datetime.timedelta(days=1)
)
)
# add two milestone answers
session.add(
MilestoneAnswer(
answer_session_id=4, milestone_id=7, milestone_group_id=2, answer=2
)
)
session.add(
MilestoneAnswer(
answer_session_id=4, milestone_id=2, milestone_group_id=1, answer=3
)
)

Expand Down
55 changes: 46 additions & 9 deletions mondey_backend/tests/utils/test_scores.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from datetime import datetime
from datetime import timedelta

from sqlmodel import select

from mondey_backend.models.milestones import MilestoneAgeScore
from mondey_backend.models.milestones import MilestoneAnswerSession
from mondey_backend.models.milestones import MilestoneGroupAgeScore
from mondey_backend.models.milestones import MilestoneGroupAgeScoreCollection
from mondey_backend.routers.scores import TrafficLight
from mondey_backend.routers.scores import compute_feedback_simple
from mondey_backend.routers.scores import compute_milestonegroup_feedback_summary
Expand Down Expand Up @@ -57,20 +63,51 @@ def test_compute_summary_milestonegroup_feedback_for_answersession(session):
assert feedback[1] == TrafficLight.yellowWithCaveat.value
assert len(feedback) == 1

# # same as above, but for answers 4, 3 -> 3.5 ==> green
# feedback = compute_milestonegroup_feedback_summary(
# session,
# child_id=1,
# answersession_id=2
# )
# assert len(feedback) == 1
# assert feedback[1] == TrafficLight.green.value
# same as above, but for answers 4, 3 -> 3.5 ==> green
feedback = compute_milestonegroup_feedback_summary(
session, child_id=1, answersession_id=2
)
assert len(feedback) == 1
assert feedback[1] == TrafficLight.green.value


def test_compute_summary_milestonegroup_feedback_for_answersession_no_statistics(
session,
):
assert 5 == 9
feedback = compute_milestonegroup_feedback_summary(
session, child_id=3, answersession_id=3
)

# child is 42 months old, for which there is no data. Hence the feedback is invalid
assert len(feedback) == 1
assert feedback[2] == TrafficLight.invalid.value

# check that the statistics have been updated
statistics = session.exec(
select(MilestoneGroupAgeScoreCollection).where(
MilestoneGroupAgeScoreCollection.milestone_group_id == 2
)
).all()
assert len(statistics) == 1
assert statistics[0].created_at >= datetime.now() - timedelta(
minutes=3
) # can be at max 3 min old

for i, score in enumerate(statistics[0].scores):
if i in [8, 9]:
assert score.avg_score > 0
assert score.stddev_score > 0
assert score.count == 2

if i == 65:
assert score.avg_score > 0
assert score.stddev_score == 0
assert score.count == 1

if i not in [8, 9, 65]:
assert score.avg_score == 0
assert score.stddev_score == 0
assert score.count == 0


# def test_compute_detailed_milestonegroup_feedback_for_answersession(session):
Expand Down

0 comments on commit 7b948a5

Please sign in to comment.