Commit 43fecd172e1679eed85faa2b1ac009c45eda9172
1 parent
88531302
Exists in
master
and in
1 other branch
code cleaning and refactoring.
changed filename knowledge.py to student.py
Showing
4 changed files
with
248 additions
and
248 deletions
Show diff stats
aprendizations/__init__.py
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} | ... | ... |
... | ... | @@ -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] | ... | ... |