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

## 55 statements

, created at 2023-01-27 05:44 +0100

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)