Coverage for src/ensae_teaching_cs/special/student.py: 91%

55 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-01-27 05:44 +0100

1""" 

2@file 

3@brief 

4""" 

5import numpy 

6 

7 

8class Student: 

9 """ 

10 Class Student, it has: 

11 

12 * `qna`: dictionary {question: answer} 

13 

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!) 

17 

18 .. runpython:: 

19 :showcode: 

20 

21 from ensae_teaching_cs.special.student import Student 

22 

23 st = Student({'q1': 0.6, 'q2': 0.7, 'q3': 0.1}) 

24 print(st) 

25 

26 """ 

27 

28 def __init__(self, qna): 

29 self.qna = qna 

30 

31 def __repr__(self): 

32 "usual" 

33 return f"{self.__class__.__name__}({self.qna!r})" 

34 

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. 

41 

42 `{ ('q1', 'q2'): { (True, False): 1, (False, False): 2 } }` 

43 

44 This means there were 3 students, 1 was right at q1 and wrong at q2, 

45 2 were wrong at both questions. 

46 

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 

50 

51 .. runpython:: 

52 :showcode: 

53 

54 from ensae_teaching_cs.special.student import Student 

55 from pprint import pprint 

56 

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() 

60 

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 

73 

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 

81 

82 return res 

83 

84 def to_matrix(self, names=None): 

85 """ 

86 Returns a names, vect. 

87 

88 * names is dictionary `{'q1': row_index}` 

89 * vect is a vector: mat[row_index] is the answer to question q1 

90 

91 :param names: mapping between rows and questions 

92 :return: names or names, new or updated counter 

93 

94 .. runpython:: 

95 :showcode: 

96 

97 from ensae_teaching_cs.special.student import Student 

98 

99 st = Student({'q1': 0.6, 'q2': 0.7, 'q3': 0.1}) 

100 names, mat = st.to_matrix() 

101 

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 

110 

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 

115 

116 def count_anwers_matrix(self, threshold=0.5, counter=None, names=None): 

117 """ 

118 Returns a names, mat. 

119 

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 

130 

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 

135 

136 .. runpython:: 

137 :showcode: 

138 

139 from ensae_teaching_cs.special.student import Student 

140 

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() 

144 

145 print(names) 

146 print(mat) 

147 

148 The following code compares this method to the previous one. 

149 

150 .. runpython:: 

151 :showcode: 

152 

153 from pyinstrument import Profiler 

154 from ensae_teaching_cs.special.student import Student 

155 

156 

157 students = [Student.random_student(80) for i in range(50)] 

158 

159 profiler = Profiler() 

160 profiler.start() 

161 

162 for n in range(10): 

163 mat2 = {} 

164 for st in students: 

165 st.count_anwers(counter=mat2) 

166 

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) 

172 

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) 

178 

179 if counter is None: 

180 res = numpy.zeros((4, len(names), len(names)), dtype=numpy.float64) 

181 else: 

182 res = counter 

183 

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 

192 

193 @staticmethod 

194 def random_student(n=100): 

195 """ 

196 Returns a student with random answers. 

197 

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)