Coverage for src/ensae_teaching_cs/special/student.py: 91%
55 statements
« prev ^ index » next coverage.py v7.1.0, created at 2023-04-28 06:23 +0200
« prev ^ index » next coverage.py v7.1.0, created at 2023-04-28 06:23 +0200
1"""
2@file
3@brief
4"""
5import numpy
8class Student:
9 """
10 Class Student, it has:
12 * `qna`: dictionary {question: answer}
14 `answer` is a float in [0, 1], 0 means the student
15 failed to answer, 1 means the student is right,
16 in ]0, 1[, we don't know for sure. (We are human!)
18 .. runpython::
19 :showcode:
21 from ensae_teaching_cs.special.student import Student
23 st = Student({'q1': 0.6, 'q2': 0.7, 'q3': 0.1})
24 print(st)
26 """
28 def __init__(self, qna):
29 self.qna = qna
31 def __repr__(self):
32 "usual"
33 return f"{self.__class__.__name__}({self.qna!r})"
35 def count_anwers(self, threshold=0.5, counter=None):
36 """
37 Returns a dictionary `{ ('q1', 'q2'): { (True, False): 1 } }`.
38 That means the student was True at question q1 and False at question q2.
39 If counter is not None, this dictionary is added to the same
40 dictionary computed with an other students.
42 `{ ('q1', 'q2'): { (True, False): 1, (False, False): 2 } }`
44 This means there were 3 students, 1 was right at q1 and wrong at q2,
45 2 were wrong at both questions.
47 :param threshold: threshold above which the answer is valid
48 :param counter: existing counter, added to these counts
49 :return: new or updated counter
51 .. runpython::
52 :showcode:
54 from ensae_teaching_cs.special.student import Student
55 from pprint import pprint
57 st1 = Student({'q1': 0.6, 'q2': 0.7, 'q3': 0.1})
58 st2 = Student({'q1': 0.6, 'q2': 0.2, 'q3': 0.1})
59 mat = st1.count_anwers()
61 pprint(mat)
62 """
63 if counter is None:
64 res = {}
65 else:
66 res = counter
67 for q1, a1 in self.qna.items():
68 b1 = a1 >= 0.5
69 for q2, a2 in self.qna.items():
70 b2 = a2 >= 0.5
71 if q1 == q2:
72 continue
74 key = q1, q2
75 if key not in res:
76 res[key] = {}
77 key2 = b1, b2
78 if key2 not in res[key]:
79 res[key][key2] = 0
80 res[key][key2] += 1
82 return res
84 def to_matrix(self, names=None):
85 """
86 Returns a names, vect.
88 * names is dictionary `{'q1': row_index}`
89 * vect is a vector: mat[row_index] is the answer to question q1
91 :param names: mapping between rows and questions
92 :return: names or names, new or updated counter
94 .. runpython::
95 :showcode:
97 from ensae_teaching_cs.special.student import Student
99 st = Student({'q1': 0.6, 'q2': 0.7, 'q3': 0.1})
100 names, mat = st.to_matrix()
102 print(names)
103 print(mat)
104 """
105 if names is None:
106 sorted_names = list(sorted(self.qna))
107 names = {}
108 for i, name in enumerate(sorted_names):
109 names[name] = i
111 mat = numpy.zeros(len(names), dtype=numpy.float64)
112 for q, a in self.qna.items():
113 mat[names[q]] = a
114 return names, mat
116 def count_anwers_matrix(self, threshold=0.5, counter=None, names=None):
117 """
118 Returns a names, mat.
120 * names is dictionary `{'q1': row_index}`
121 * mat is a matrix:
122 * mat[0, q1_index, q2_index] is the number of times
123 students were right at questions q1, q2
124 * mat[1, q1_index, q2_index] is the number of times
125 students were right at question q1 and wrong at question q2
126 * mat[2, q1_index, q2_index] is the number of times
127 students were wrong at question q1 and right at question q2
128 * mat[3, q1_index, q2_index] is the number of times
129 students were wring at both questions
131 :param threshold: threshold above which the answer is valid
132 :param counter: existing counter, added to these counts
133 :param names: mapping between rows and questions
134 :return: names or names, new or updated counter
136 .. runpython::
137 :showcode:
139 from ensae_teaching_cs.special.student import Student
141 st1 = Student({'q1': 0.6, 'q2': 0.7, 'q3': 0.1})
142 st2 = Student({'q1': 0.6, 'q2': 0.2, 'q3': 0.1})
143 names, mat = st1.count_anwers_matrix()
145 print(names)
146 print(mat)
148 The following code compares this method to the previous one.
150 .. runpython::
151 :showcode:
153 from pyinstrument import Profiler
154 from ensae_teaching_cs.special.student import Student
157 students = [Student.random_student(80) for i in range(50)]
159 profiler = Profiler()
160 profiler.start()
162 for n in range(10):
163 mat2 = {}
164 for st in students:
165 st.count_anwers(counter=mat2)
167 for n in range(10):
168 mat3 = None
169 names = None
170 for st in students:
171 names, mat3 = st.count_anwers_matrix(counter=mat3, names=names)
173 profiler.stop()
174 print(profiler.output_text())
175 """
176 names, mat = self.to_matrix(names)
177 mat = (mat >= threshold).reshape(-1, 1).astype(numpy.int64)
179 if counter is None:
180 res = numpy.zeros((4, len(names), len(names)), dtype=numpy.float64)
181 else:
182 res = counter
184 neg_mat = 1 - mat
185 tmat = mat.T
186 tneg_mat = neg_mat.T
187 res[0, :, :] += mat @ tmat # numpy.dot(mat, tmat)
188 res[1, :, :] += mat @ tneg_mat
189 res[2, :, :] += neg_mat @ tmat
190 res[3, :, :] += neg_mat @ tneg_mat
191 return names, res
193 @staticmethod
194 def random_student(n=100):
195 """
196 Returns a student with random answers.
198 :param n: number of questions
199 :return: *Student*
200 """
201 rnd = numpy.random.rand(n)
202 qna = {}
203 for i in range(n):
204 qna[i] = rnd[i]
205 return Student(qna)