Commit 43fecd172e1679eed85faa2b1ac009c45eda9172

Authored by Miguel Barão
1 parent 88531302
Exists in master and in 1 other branch dev

code cleaning and refactoring.

changed filename knowledge.py to student.py
aprendizations/__init__.py
... ... @@ -30,7 +30,7 @@ are progressively uncovered as the students progress.
30 30 '''
31 31  
32 32 APP_NAME = 'aprendizations'
33   -APP_VERSION = '2019.05.dev3'
  33 +APP_VERSION = '2019.07.dev1'
34 34 APP_DESCRIPTION = __doc__
35 35  
36 36 __author__ = 'Miguel Barão'
... ...
aprendizations/knowledge.py
... ... @@ -1,236 +0,0 @@
1   -
2   -# python standard library
3   -import random
4   -from datetime import datetime
5   -import logging
6   -
7   -# third party libraries
8   -import networkx as nx
9   -
10   -# setup logger for this module
11   -logger = logging.getLogger(__name__)
12   -
13   -
14   -# ----------------------------------------------------------------------------
15   -# kowledge state of each student....??
16   -# Contains:
17   -# state - dict of topics with state of unlocked topics
18   -# deps - access to dependency graph shared between students
19   -# topic_sequence - list with the order of recommended topics
20   -# ----------------------------------------------------------------------------
21   -class StudentKnowledge(object):
22   - # =======================================================================
23   - # methods that update state
24   - # =======================================================================
25   - def __init__(self, deps, factory, state={}):
26   - self.deps = deps # shared dependency graph
27   - self.factory = factory # question factory
28   - self.state = state # {'topic': {'level': 0.5, 'date': datetime}, ...}
29   -
30   - self.update_topic_levels() # applies forgetting factor
31   - self.unlock_topics() # whose dependencies have been completed
32   - self.topic_sequence = self.recommend_topic_sequence() # ['ref1', ...]
33   - self.current_topic = None
34   -
35   - # ------------------------------------------------------------------------
36   - # Updates the proficiency levels of the topics, with forgetting factor
37   - # FIXME no dependencies are considered yet...
38   - # ------------------------------------------------------------------------
39   - def update_topic_levels(self):
40   - now = datetime.now()
41   - for tref, s in self.state.items():
42   - dt = now - s['date']
43   - s['level'] *= 0.98 ** dt.days # forgetting factor
44   -
45   - # ------------------------------------------------------------------------
46   - # Unlock topics whose dependencies are satisfied (> min_level)
47   - # ------------------------------------------------------------------------
48   - def unlock_topics(self):
49   - for topic in self.deps.nodes():
50   - if topic not in self.state: # if locked
51   - pred = self.deps.predecessors(topic)
52   - min_level = self.deps.node[topic]['min_level']
53   - if all(d in self.state and self.state[d]['level'] > min_level
54   - for d in pred): # all deps are greater than min_level
55   -
56   - self.state[topic] = {
57   - 'level': 0.0, # unlocked
58   - 'date': datetime.now()
59   - }
60   - logger.debug(f'[unlock_topics] Unlocked "{topic}".')
61   - # else: # lock this topic if deps do not satisfy min_level
62   - # del self.state[topic]
63   -
64   - # ------------------------------------------------------------------------
65   - # Start a new topic.
66   - # questions: list of generated questions to do in the topic
67   - # current_question: the current question to be presented
68   - # ------------------------------------------------------------------------
69   - async def start_topic(self, topic):
70   - logger.debug(f'[start_topic] topic "{topic}"')
71   -
72   - if self.current_topic == topic:
73   - logger.info('Restarting current topic is not allowed.')
74   - return
75   -
76   - # do not allow locked topics
77   - if self.is_locked(topic):
78   - logger.debug(f'[start_topic] topic "{topic}" is locked')
79   - return
80   -
81   - # starting new topic
82   - self.current_topic = topic
83   - self.correct_answers = 0
84   - self.wrong_answers = 0
85   -
86   - t = self.deps.node[topic]
87   - k = t['choose']
88   - if t['shuffle_questions']:
89   - questions = random.sample(t['questions'], k=k)
90   - else:
91   - questions = t['questions'][:k]
92   - logger.debug(f'[start_topic] questions: {", ".join(questions)}')
93   -
94   - # synchronous
95   - # self.questions = [self.factory[ref].generate()
96   - # for ref in questions]
97   -
98   - # asynchronous:
99   - self.questions = [await self.factory[ref].generate_async()
100   - for ref in questions]
101   -
102   - n = len(self.questions)
103   - logger.debug(f'[start_topic] generated {n} questions')
104   -
105   - # get first question
106   - self.next_question()
107   -
108   - # ------------------------------------------------------------------------
109   - # The topic has finished and there are no more questions.
110   - # The topic level is updated in state and unlocks are performed.
111   - # The current topic is unchanged.
112   - # ------------------------------------------------------------------------
113   - def finish_topic(self):
114   - logger.debug(f'[finish_topic] current_topic {self.current_topic}')
115   -
116   - self.state[self.current_topic] = {
117   - 'date': datetime.now(),
118   - 'level': self.correct_answers / (self.correct_answers +
119   - self.wrong_answers)
120   - }
121   - # self.current_topic = None
122   - self.unlock_topics()
123   -
124   - # ------------------------------------------------------------------------
125   - # corrects current question with provided answer.
126   - # implements the logic:
127   - # - if answer ok, goes to next question
128   - # - if wrong, counts number of tries. If exceeded, moves on.
129   - # ------------------------------------------------------------------------
130   - async def check_answer(self, answer):
131   - logger.debug('[check_answer]')
132   -
133   - q = self.current_question
134   - q['answer'] = answer
135   - q['finish_time'] = datetime.now()
136   - await q.correct_async()
137   - logger.debug(f'[check_answer] Grade {q["grade"]:.2} in {q["ref"]}')
138   -
139   - if q['grade'] > 0.999:
140   - self.correct_answers += 1
141   - self.next_question()
142   - action = 'right'
143   -
144   - else:
145   - self.wrong_answers += 1
146   - self.current_question['tries'] -= 1
147   -
148   - if self.current_question['tries'] > 0:
149   - action = 'try_again'
150   - else:
151   - action = 'wrong'
152   - if self.current_question['append_wrong']:
153   - logger.debug('[check_answer] Wrong, append new instance')
154   - self.questions.append(self.factory[q['ref']].generate())
155   - self.next_question()
156   -
157   - # returns corrected question (not new one) which might include comments
158   - return q, action
159   -
160   - # ------------------------------------------------------------------------
161   - # Move to next question
162   - # ------------------------------------------------------------------------
163   - def next_question(self):
164   - try:
165   - self.current_question = self.questions.pop(0)
166   - except IndexError:
167   - self.current_question = None
168   - self.finish_topic()
169   - else:
170   - self.current_question['start_time'] = datetime.now()
171   - default_maxtries = self.deps.nodes[self.current_topic]['max_tries']
172   - maxtries = self.current_question.get('max_tries', default_maxtries)
173   - self.current_question['tries'] = maxtries
174   - logger.debug(f'[next_question] "{self.current_question["ref"]}"')
175   -
176   - return self.current_question # question or None
177   -
178   - # ========================================================================
179   - # pure functions of the state (no side effects)
180   - # ========================================================================
181   -
182   - def topic_has_finished(self):
183   - return self.current_question is None
184   -
185   - # ------------------------------------------------------------------------
186   - # compute recommended sequence of topics ['a', 'b', ...]
187   - # ------------------------------------------------------------------------
188   - def recommend_topic_sequence(self, target=None):
189   - tt = list(nx.topological_sort(self.deps))
190   - unlocked = [t for t in tt if t in self.state]
191   - locked = [t for t in tt if t not in unlocked]
192   - return unlocked + locked
193   -
194   - # ------------------------------------------------------------------------
195   - def get_current_question(self):
196   - return self.current_question
197   -
198   - # ------------------------------------------------------------------------
199   - def get_current_topic(self):
200   - return self.current_topic
201   -
202   - # ------------------------------------------------------------------------
203   - def is_locked(self, topic):
204   - return topic not in self.state
205   -
206   - # ------------------------------------------------------------------------
207   - # Return list of {ref: 'xpto', name: 'long name', leve: 0.5}
208   - # Levels are in the interval [0, 1] if unlocked or None if locked.
209   - # Topics unlocked but not yet done have level 0.0.
210   - # ------------------------------------------------------------------------
211   - def get_knowledge_state(self):
212   - return [{
213   - 'ref': ref,
214   - 'type': self.deps.nodes[ref]['type'],
215   - 'name': self.deps.nodes[ref]['name'],
216   - 'level': self.state[ref]['level'] if ref in self.state else None
217   - } for ref in self.topic_sequence]
218   -
219   - # ------------------------------------------------------------------------
220   - def get_topic_progress(self):
221   - return self.correct_answers / (1 + self.correct_answers +
222   - len(self.questions))
223   -
224   - # ------------------------------------------------------------------------
225   - def get_topic_level(self, topic):
226   - return self.state[topic]['level']
227   -
228   - # ------------------------------------------------------------------------
229   - def get_topic_date(self, topic):
230   - return self.state[topic]['date']
231   -
232   - # ------------------------------------------------------------------------
233   - # Recommends a topic to practice/learn from the state.
234   - # ------------------------------------------------------------------------
235   - # def get_recommended_topic(self): # FIXME untested
236   - # return min(self.state.items(), key=lambda x: x[1]['level'])[0]
aprendizations/learnapp.py
... ... @@ -15,7 +15,7 @@ import networkx as nx
15 15  
16 16 # this project
17 17 from .models import Student, Answer, Topic, StudentTopic
18   -from .knowledge import StudentKnowledge
  18 +from .student import StudentState
19 19 from .questions import QFactory
20 20 from .tools import load_yaml
21 21  
... ... @@ -155,8 +155,8 @@ class LearnApp(object):
155 155 self.online[uid] = {
156 156 'number': uid,
157 157 'name': name,
158   - 'state': StudentKnowledge(deps=self.deps, factory=self.factory,
159   - state=state),
  158 + 'state': StudentState(deps=self.deps, factory=self.factory,
  159 + state=state),
160 160 'counter': counter + 1, # counts simultaneous logins
161 161 }
162 162  
... ... @@ -441,16 +441,16 @@ class LearnApp(object):
441 441 total_topics = s.query(Topic).count()
442 442  
443 443 # answer performance
444   - totalans = dict(s.query(Answer.student_id, sa.func.count(Answer.ref)).
445   - group_by(Answer.student_id).
446   - all())
447   - rightans = dict(s.query(Answer.student_id, sa.func.count(Answer.ref)).
448   - filter(Answer.grade == 1.0).
449   - group_by(Answer.student_id).
450   - all())
  444 + total = dict(s.query(Answer.student_id, sa.func.count(Answer.ref)).
  445 + group_by(Answer.student_id).
  446 + all())
  447 + right = dict(s.query(Answer.student_id, sa.func.count(Answer.ref)).
  448 + filter(Answer.grade == 1.0).
  449 + group_by(Answer.student_id).
  450 + all())
451 451  
452 452 # compute percentage of right answers
453   - perf = {uid: rightans.get(uid, 0.0)/totalans[uid] for uid in totalans}
  453 + perf = {uid: right.get(uid, 0.0)/total[uid] for uid in total}
454 454  
455 455 # compute topic progress
456 456 prog = {s[0]: 0.0 for s in students}
... ...
aprendizations/student.py 0 → 100644
... ... @@ -0,0 +1,236 @@
  1 +
  2 +# python standard library
  3 +import random
  4 +from datetime import datetime
  5 +import logging
  6 +
  7 +# third party libraries
  8 +import networkx as nx
  9 +
  10 +# setup logger for this module
  11 +logger = logging.getLogger(__name__)
  12 +
  13 +
  14 +# ----------------------------------------------------------------------------
  15 +# kowledge state of each student....??
  16 +# Contains:
  17 +# state - dict of topics with state of unlocked topics
  18 +# deps - access to dependency graph shared between students
  19 +# topic_sequence - list with the order of recommended topics
  20 +# ----------------------------------------------------------------------------
  21 +class StudentState(object):
  22 + # =======================================================================
  23 + # methods that update state
  24 + # =======================================================================
  25 + def __init__(self, deps, factory, state={}):
  26 + self.deps = deps # shared dependency graph
  27 + self.factory = factory # question factory
  28 + self.state = state # {'topic': {'level': 0.5, 'date': datetime}, ...}
  29 +
  30 + self.update_topic_levels() # applies forgetting factor
  31 + self.unlock_topics() # whose dependencies have been completed
  32 + self.topic_sequence = self.recommend_topic_sequence() # ['ref1', ...]
  33 + self.current_topic = None
  34 +
  35 + # ------------------------------------------------------------------------
  36 + # Updates the proficiency levels of the topics, with forgetting factor
  37 + # FIXME no dependencies are considered yet...
  38 + # ------------------------------------------------------------------------
  39 + def update_topic_levels(self):
  40 + now = datetime.now()
  41 + for tref, s in self.state.items():
  42 + dt = now - s['date']
  43 + s['level'] *= 0.98 ** dt.days # forgetting factor
  44 +
  45 + # ------------------------------------------------------------------------
  46 + # Unlock topics whose dependencies are satisfied (> min_level)
  47 + # ------------------------------------------------------------------------
  48 + def unlock_topics(self):
  49 + for topic in self.deps.nodes():
  50 + if topic not in self.state: # if locked
  51 + pred = self.deps.predecessors(topic)
  52 + min_level = self.deps.node[topic]['min_level']
  53 + if all(d in self.state and self.state[d]['level'] > min_level
  54 + for d in pred): # all deps are greater than min_level
  55 +
  56 + self.state[topic] = {
  57 + 'level': 0.0, # unlocked
  58 + 'date': datetime.now()
  59 + }
  60 + logger.debug(f'[unlock_topics] Unlocked "{topic}".')
  61 + # else: # lock this topic if deps do not satisfy min_level
  62 + # del self.state[topic]
  63 +
  64 + # ------------------------------------------------------------------------
  65 + # Start a new topic.
  66 + # questions: list of generated questions to do in the topic
  67 + # current_question: the current question to be presented
  68 + # ------------------------------------------------------------------------
  69 + async def start_topic(self, topic):
  70 + logger.debug(f'[start_topic] topic "{topic}"')
  71 +
  72 + if self.current_topic == topic:
  73 + logger.info('Restarting current topic is not allowed.')
  74 + return
  75 +
  76 + # do not allow locked topics
  77 + if self.is_locked(topic):
  78 + logger.debug(f'[start_topic] topic "{topic}" is locked')
  79 + return
  80 +
  81 + # starting new topic
  82 + self.current_topic = topic
  83 + self.correct_answers = 0
  84 + self.wrong_answers = 0
  85 +
  86 + t = self.deps.node[topic]
  87 + k = t['choose']
  88 + if t['shuffle_questions']:
  89 + questions = random.sample(t['questions'], k=k)
  90 + else:
  91 + questions = t['questions'][:k]
  92 + logger.debug(f'[start_topic] questions: {", ".join(questions)}')
  93 +
  94 + # synchronous
  95 + # self.questions = [self.factory[ref].generate()
  96 + # for ref in questions]
  97 +
  98 + # asynchronous:
  99 + self.questions = [await self.factory[ref].generate_async()
  100 + for ref in questions]
  101 +
  102 + n = len(self.questions)
  103 + logger.debug(f'[start_topic] generated {n} questions')
  104 +
  105 + # get first question
  106 + self.next_question()
  107 +
  108 + # ------------------------------------------------------------------------
  109 + # The topic has finished and there are no more questions.
  110 + # The topic level is updated in state and unlocks are performed.
  111 + # The current topic is unchanged.
  112 + # ------------------------------------------------------------------------
  113 + def finish_topic(self):
  114 + logger.debug(f'[finish_topic] current_topic {self.current_topic}')
  115 +
  116 + self.state[self.current_topic] = {
  117 + 'date': datetime.now(),
  118 + 'level': self.correct_answers / (self.correct_answers +
  119 + self.wrong_answers)
  120 + }
  121 + # self.current_topic = None
  122 + self.unlock_topics()
  123 +
  124 + # ------------------------------------------------------------------------
  125 + # corrects current question with provided answer.
  126 + # implements the logic:
  127 + # - if answer ok, goes to next question
  128 + # - if wrong, counts number of tries. If exceeded, moves on.
  129 + # ------------------------------------------------------------------------
  130 + async def check_answer(self, answer):
  131 + logger.debug('[check_answer]')
  132 +
  133 + q = self.current_question
  134 + q['answer'] = answer
  135 + q['finish_time'] = datetime.now()
  136 + await q.correct_async()
  137 + logger.debug(f'[check_answer] Grade {q["grade"]:.2} in {q["ref"]}')
  138 +
  139 + if q['grade'] > 0.999:
  140 + self.correct_answers += 1
  141 + self.next_question()
  142 + action = 'right'
  143 +
  144 + else:
  145 + self.wrong_answers += 1
  146 + self.current_question['tries'] -= 1
  147 +
  148 + if self.current_question['tries'] > 0:
  149 + action = 'try_again'
  150 + else:
  151 + action = 'wrong'
  152 + if self.current_question['append_wrong']:
  153 + logger.debug('[check_answer] Wrong, append new instance')
  154 + self.questions.append(self.factory[q['ref']].generate())
  155 + self.next_question()
  156 +
  157 + # returns corrected question (not new one) which might include comments
  158 + return q, action
  159 +
  160 + # ------------------------------------------------------------------------
  161 + # Move to next question
  162 + # ------------------------------------------------------------------------
  163 + def next_question(self):
  164 + try:
  165 + self.current_question = self.questions.pop(0)
  166 + except IndexError:
  167 + self.current_question = None
  168 + self.finish_topic()
  169 + else:
  170 + self.current_question['start_time'] = datetime.now()
  171 + default_maxtries = self.deps.nodes[self.current_topic]['max_tries']
  172 + maxtries = self.current_question.get('max_tries', default_maxtries)
  173 + self.current_question['tries'] = maxtries
  174 + logger.debug(f'[next_question] "{self.current_question["ref"]}"')
  175 +
  176 + return self.current_question # question or None
  177 +
  178 + # ========================================================================
  179 + # pure functions of the state (no side effects)
  180 + # ========================================================================
  181 +
  182 + def topic_has_finished(self):
  183 + return self.current_question is None
  184 +
  185 + # ------------------------------------------------------------------------
  186 + # compute recommended sequence of topics ['a', 'b', ...]
  187 + # ------------------------------------------------------------------------
  188 + def recommend_topic_sequence(self, target=None):
  189 + tt = list(nx.topological_sort(self.deps))
  190 + unlocked = [t for t in tt if t in self.state]
  191 + locked = [t for t in tt if t not in unlocked]
  192 + return unlocked + locked
  193 +
  194 + # ------------------------------------------------------------------------
  195 + def get_current_question(self):
  196 + return self.current_question
  197 +
  198 + # ------------------------------------------------------------------------
  199 + def get_current_topic(self):
  200 + return self.current_topic
  201 +
  202 + # ------------------------------------------------------------------------
  203 + def is_locked(self, topic):
  204 + return topic not in self.state
  205 +
  206 + # ------------------------------------------------------------------------
  207 + # Return list of {ref: 'xpto', name: 'long name', leve: 0.5}
  208 + # Levels are in the interval [0, 1] if unlocked or None if locked.
  209 + # Topics unlocked but not yet done have level 0.0.
  210 + # ------------------------------------------------------------------------
  211 + def get_knowledge_state(self):
  212 + return [{
  213 + 'ref': ref,
  214 + 'type': self.deps.nodes[ref]['type'],
  215 + 'name': self.deps.nodes[ref]['name'],
  216 + 'level': self.state[ref]['level'] if ref in self.state else None
  217 + } for ref in self.topic_sequence]
  218 +
  219 + # ------------------------------------------------------------------------
  220 + def get_topic_progress(self):
  221 + return self.correct_answers / (1 + self.correct_answers +
  222 + len(self.questions))
  223 +
  224 + # ------------------------------------------------------------------------
  225 + def get_topic_level(self, topic):
  226 + return self.state[topic]['level']
  227 +
  228 + # ------------------------------------------------------------------------
  229 + def get_topic_date(self, topic):
  230 + return self.state[topic]['date']
  231 +
  232 + # ------------------------------------------------------------------------
  233 + # Recommends a topic to practice/learn from the state.
  234 + # ------------------------------------------------------------------------
  235 + # def get_recommended_topic(self): # FIXME untested
  236 + # return min(self.state.items(), key=lambda x: x[1]['level'])[0]
... ...