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,7 +30,7 @@ are progressively uncovered as the students progress.
30 ''' 30 '''
31 31
32 APP_NAME = 'aprendizations' 32 APP_NAME = 'aprendizations'
33 -APP_VERSION = '2019.05.dev3' 33 +APP_VERSION = '2019.07.dev1'
34 APP_DESCRIPTION = __doc__ 34 APP_DESCRIPTION = __doc__
35 35
36 __author__ = 'Miguel Barão' 36 __author__ = 'Miguel Barão'
aprendizations/knowledge.py
@@ -1,236 +0,0 @@ @@ -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,7 +15,7 @@ import networkx as nx
15 15
16 # this project 16 # this project
17 from .models import Student, Answer, Topic, StudentTopic 17 from .models import Student, Answer, Topic, StudentTopic
18 -from .knowledge import StudentKnowledge 18 +from .student import StudentState
19 from .questions import QFactory 19 from .questions import QFactory
20 from .tools import load_yaml 20 from .tools import load_yaml
21 21
@@ -155,8 +155,8 @@ class LearnApp(object): @@ -155,8 +155,8 @@ class LearnApp(object):
155 self.online[uid] = { 155 self.online[uid] = {
156 'number': uid, 156 'number': uid,
157 'name': name, 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 'counter': counter + 1, # counts simultaneous logins 160 'counter': counter + 1, # counts simultaneous logins
161 } 161 }
162 162
@@ -441,16 +441,16 @@ class LearnApp(object): @@ -441,16 +441,16 @@ class LearnApp(object):
441 total_topics = s.query(Topic).count() 441 total_topics = s.query(Topic).count()
442 442
443 # answer performance 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 # compute percentage of right answers 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 # compute topic progress 455 # compute topic progress
456 prog = {s[0]: 0.0 for s in students} 456 prog = {s[0]: 0.0 for s in students}
aprendizations/student.py 0 → 100644
@@ -0,0 +1,236 @@ @@ -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]