# Výchozí parametry notebooku pro Papermill
FILENAME = 'bilin1.json'
DEADLINE = '2020-05-14 23:59:00+0100'
POINTS_TO_PASS = 16 # bodů
CLUSTER_THRESHOLD = 120 # minut
# Parameters
FILENAME = "bilin1.json"
DEADLINE = "2020-05-14 23:59:00+0100"
POINTS_TO_PASS = 16
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import fclusterdata
Načtení dat z JSON formátu poskytovaného MARAST API, upravení datových typů sloupců a zpracování deadline.
df = pd.read_json(FILENAME)
df.created_at = pd.to_datetime(df.created_at, utc=True)
df.updated_at = pd.to_datetime(df.updated_at, utc=True)
df.answered_at = pd.to_datetime(df.answered_at, utc=True)
df.score = df.score.fillna(0).astype(np.int64)
deadline = pd.to_datetime(DEADLINE, utc=True)
Dopočtení dalších užitečných údajů (doba od vytvoření otázky do zodpovězení, doba zbývající do deadline).
for idx, row in df.iterrows():
# doba od vytvoření otázky od zodpovězení (v minutách)
if row['answered_at']:
df.at[idx, 'duration'] = (row['answered_at'] - row['created_at']).seconds / 60
# doba od vytvoření otázky do deadline
df.at[idx, 'to_deadline'] = (deadline - row['created_at']).total_seconds() / 60
Souhrn sloupců.
df.columns
Index(['problem_id', 'anonymous_user_id', 'is_open', 'is_correctly_answered', 'score', 'state', 'kind', 'solution', 'created_at', 'updated_at', 'answered_at', 'is_modified', 'duration', 'to_deadline'], dtype='object')
Přehled datových typů sloupců pro kontrolu.
df.dtypes
problem_id int64 anonymous_user_id object is_open bool is_correctly_answered bool score int64 state object kind object solution object created_at datetime64[ns, UTC] updated_at datetime64[ns, UTC] answered_at datetime64[ns, UTC] is_modified bool duration float64 to_deadline float64 dtype: object
Extrakce studentů a příkladů vyskytujících se v datech.
students = df.anonymous_user_id.drop_duplicates().values
problem_ids = df.problem_id.drop_duplicates().values
Jednoduché celkové počty studentů, odpovědí a příkladů.
print('Počet studentů: ', len(students))
print('Počet odpovědí: ', len(df))
print('Počet příkladů: ', len(problem_ids))
Počet studentů: 532 Počet odpovědí: 14243 Počet příkladů: 62
Kdy jsou studenti nejaktivnější co se denní hodiny týče?
ax = df.created_at.apply(lambda t: t.hour + t.minute / 60).hist(bins=24, figsize=(10, 5))
ax.set_xlabel('Hodina dne')
ax.set_ylabel('Počet odpovědí')
ax.set_title('Aktivita studentů během dne')
plt.xticks(range(25))
plt.show()
Jak je tato aktivita závislá na blížícím se deadline?
data = df.to_deadline.apply(lambda t: t / 60)
maxh = int(np.ceil(data.max()))
ax = data.hist(bins=maxh, figsize=(12, 5))
ax.set_xlabel('Počet hodin zbývajících do deadline')
ax.set_ylabel('Počet odpovědí')
ax.set_title('Aktivita studentů během doby přístupnosti kvízu')
plt.xticks(range(0, maxh, 24), rotation=90)
plt.show()
Předpočtení statistik příkladů.
dp = pd.DataFrame(index=problem_ids, columns=[
'displayed', 'kind', 'answers', 'correct', 'wrong', 'success_rate', 'median_time'
])
for p in problem_ids:
answers = df[(df.problem_id == p) & (df.is_open == False)]
if pd.isnull(dp.at[p, 'kind']):
dp.at[p, 'kind'] = answers.iloc[0]['kind']
dp.at[p, 'displayed'] = len(df[df.problem_id == p])
dp.at[p, 'answers'] = len(answers)
dp.at[p, 'correct'] = len(df[(df.problem_id == p) & (df.is_open == False) & (df.is_correctly_answered == True)])
dp.at[p, 'wrong'] = dp.at[p, 'answers'] - dp.at[p, 'correct']
dp.at[p, 'success_rate'] = dp.at[p, 'correct'] / dp.at[p, 'answers']
dp.at[p, 'median_time'] = answers.duration.median()
Zobrazení výsledků, seřazeno podle míry úspěšnosti (od "nejtěžšího" příkladu k "nejlehčímu"). Index (první sloupec) je MARAST ID příkladu.
with pd.option_context('display.max_rows', None, 'display.max_columns', None):
display(dp.sort_values(by=['success_rate']))
displayed | kind | answers | correct | wrong | success_rate | median_time | |
---|---|---|---|---|---|---|---|
10911 | 29 | text_field | 28 | 4 | 24 | 0.142857 | 8.9 |
4014 | 243 | multichoice | 239 | 44 | 195 | 0.1841 | 5.05 |
5222 | 239 | text_field | 234 | 64 | 170 | 0.273504 | 3.175 |
4022 | 233 | multichoice | 227 | 66 | 161 | 0.290749 | 4.63333 |
10906 | 26 | text_field | 24 | 7 | 17 | 0.291667 | 2.025 |
4034 | 247 | multichoice | 240 | 71 | 169 | 0.295833 | 3.6 |
2236 | 241 | multichoice | 236 | 79 | 157 | 0.334746 | 5.44167 |
4011 | 235 | multichoice | 232 | 87 | 145 | 0.375 | 2.475 |
2249 | 238 | multichoice | 236 | 89 | 147 | 0.377119 | 3.025 |
4027 | 254 | multichoice | 249 | 98 | 151 | 0.393574 | 3 |
7396 | 239 | multichoice | 232 | 92 | 140 | 0.396552 | 4.75 |
4026 | 244 | multichoice | 237 | 94 | 143 | 0.396624 | 4.76667 |
4036 | 242 | multichoice | 239 | 97 | 142 | 0.405858 | 5.11667 |
4002 | 239 | multichoice | 234 | 96 | 138 | 0.410256 | 4.46667 |
2281 | 225 | multichoice | 220 | 93 | 127 | 0.422727 | 4.11667 |
4035 | 225 | multichoice | 222 | 95 | 127 | 0.427928 | 3.10833 |
5221 | 233 | text_field | 226 | 105 | 121 | 0.464602 | 3.81667 |
4031 | 222 | multichoice | 219 | 107 | 112 | 0.488584 | 2.96667 |
4015 | 233 | multichoice | 225 | 110 | 115 | 0.488889 | 2.71667 |
4010 | 236 | multichoice | 229 | 112 | 117 | 0.489083 | 2.63333 |
3994 | 254 | multichoice | 248 | 125 | 123 | 0.504032 | 3.55 |
7397 | 256 | multichoice | 251 | 128 | 123 | 0.50996 | 1.6 |
4033 | 242 | multichoice | 240 | 124 | 116 | 0.516667 | 4.54167 |
4037 | 240 | multichoice | 231 | 120 | 111 | 0.519481 | 4.73333 |
4001 | 238 | multichoice | 234 | 122 | 112 | 0.521368 | 4.10833 |
4003 | 238 | multichoice | 235 | 125 | 110 | 0.531915 | 4.4 |
3995 | 237 | multichoice | 230 | 124 | 106 | 0.53913 | 4.76667 |
4000 | 244 | multichoice | 241 | 136 | 105 | 0.564315 | 1.81667 |
4013 | 238 | multichoice | 236 | 134 | 102 | 0.567797 | 3.075 |
2241 | 254 | multichoice | 248 | 144 | 104 | 0.580645 | 1.725 |
4020 | 246 | multichoice | 243 | 142 | 101 | 0.584362 | 4.21667 |
3999 | 242 | multichoice | 236 | 143 | 93 | 0.605932 | 1.7 |
2265 | 233 | multichoice | 230 | 141 | 89 | 0.613043 | 3.15 |
7395 | 248 | multichoice | 243 | 149 | 94 | 0.613169 | 4.2 |
4018 | 243 | multichoice | 239 | 147 | 92 | 0.615063 | 2.93333 |
4012 | 223 | multichoice | 219 | 135 | 84 | 0.616438 | 3.86667 |
4032 | 226 | multichoice | 222 | 137 | 85 | 0.617117 | 3.38333 |
3991 | 213 | multichoice | 211 | 133 | 78 | 0.630332 | 2.15 |
4009 | 257 | multichoice | 253 | 162 | 91 | 0.640316 | 3.8 |
4024 | 244 | multichoice | 240 | 154 | 86 | 0.641667 | 2.03333 |
3998 | 235 | multichoice | 234 | 154 | 80 | 0.65812 | 2.66667 |
2235 | 242 | multichoice | 234 | 155 | 79 | 0.662393 | 3.21667 |
2263 | 246 | multichoice | 242 | 161 | 81 | 0.665289 | 2.66667 |
3996 | 247 | multichoice | 240 | 160 | 80 | 0.666667 | 3.31667 |
2275 | 245 | multichoice | 240 | 165 | 75 | 0.6875 | 2.45833 |
4028 | 231 | multichoice | 223 | 155 | 68 | 0.695067 | 2.78333 |
2228 | 225 | multichoice | 221 | 155 | 66 | 0.701357 | 2.46667 |
4025 | 254 | multichoice | 247 | 177 | 70 | 0.716599 | 4.66667 |
2274 | 220 | multichoice | 212 | 152 | 60 | 0.716981 | 2.94167 |
10910 | 25 | multichoice | 25 | 18 | 7 | 0.72 | 1.85 |
4023 | 254 | multichoice | 249 | 182 | 67 | 0.730924 | 3.2 |
4016 | 254 | multichoice | 250 | 184 | 66 | 0.736 | 2.98333 |
4019 | 236 | multichoice | 232 | 175 | 57 | 0.75431 | 2.46667 |
3993 | 233 | multichoice | 228 | 173 | 55 | 0.758772 | 2.74167 |
4017 | 251 | multichoice | 247 | 188 | 59 | 0.761134 | 3.78333 |
4021 | 238 | multichoice | 236 | 180 | 56 | 0.762712 | 2.36667 |
2216 | 234 | multichoice | 231 | 181 | 50 | 0.78355 | 1.63333 |
3997 | 242 | multichoice | 241 | 192 | 49 | 0.79668 | 2.55 |
4029 | 262 | multichoice | 257 | 208 | 49 | 0.809339 | 2.7 |
3992 | 242 | multichoice | 239 | 204 | 35 | 0.853556 | 1.53333 |
2283 | 238 | multichoice | 234 | 201 | 33 | 0.858974 | 1.84167 |
2247 | 250 | multichoice | 248 | 226 | 22 | 0.91129 | 0.566667 |
Nejprve musíme extrahovat potřebná data. Zaznamenáme kolik odpovědí student vytvořil, kolik jich potřeboval na splnění kvízu, jak dlouho mu to od otevření kvízu trvalo, kolik času do deadline mu v okamžiku splnění kvízu zbývalo, úspěšnost jeho odpovědí a medián doby potřebné na odeslání jedné odpovědi.
ds = pd.DataFrame(index=students, columns=[
'total_questions', 'total_score', 'questions_to_success',
'minutes_to_success', 'minutes_left', 'success_rate', 'median_minutes_to_answer'
])
Nyní samotný výpočet.
for s in students:
questions = df[df.anonymous_user_id == s].copy()
ds.at[s, 'total_questions'] = len(questions)
ds.at[s, 'total_score'] = questions.score.sum()
score = 0.0
counter = 0
success = 0
for idx, q in questions.iterrows():
counter += 1
score += q['score']
if q['is_correctly_answered']:
success += 1
if q['answered_at']:
questions.at[idx, 'minutes_to_answer'] = (q['answered_at'] - q['created_at']).total_seconds() / 60
if score >= POINTS_TO_PASS:
ds.at[s, 'questions_to_success'] = counter
ds.at[s, 'minutes_to_success'] = (q['answered_at'] - questions.iloc[0]['answered_at']).total_seconds() / 60
ds.at[s, 'minutes_left'] = (deadline - q['answered_at']).total_seconds() / 60
break
ds.at[s, 'success_rate'] = success / len(questions)
ds.at[s, 'median_minutes_to_answer'] = questions.minutes_to_answer.dropna().median()
Kontrola datových typů.
ds = ds.infer_objects()
ds.dtypes
total_questions int64 total_score int64 questions_to_success float64 minutes_to_success float64 minutes_left float64 success_rate float64 median_minutes_to_answer float64 dtype: object
Náhled dat, z očividných důvodů nezobrazujeme všechny.
ds
total_questions | total_score | questions_to_success | minutes_to_success | minutes_left | success_rate | median_minutes_to_answer | |
---|---|---|---|---|---|---|---|
85eecf84d689d21fa27283a1a61be979 | 23 | 17 | 20.0 | 121.050000 | 71328.166667 | 0.695652 | 2.350000 |
bf9a2d34f2d553e01834cccc0ee29e01 | 34 | 16 | 34.0 | 3118.533333 | 68327.600000 | 0.470588 | 7.191667 |
283b9b9390a502d564eef1ef75c7b80d | 44 | 16 | 43.0 | 53262.366667 | 16480.750000 | 0.363636 | 7.300000 |
20314c01c1fdfe3bebe0a5c7c7bd1721 | 180 | 125 | 22.0 | 76.950000 | 71352.033333 | 0.088889 | 2.441667 |
94261009f7184d107bd03a9781a587c5 | 20 | 17 | 19.0 | 1647.550000 | 69781.283333 | 0.800000 | 4.266667 |
... | ... | ... | ... | ... | ... | ... | ... |
b02def4f8d5e7bbdc0fac260a9ea3b44 | 24 | 16 | 23.0 | 377.650000 | 337.766667 | 0.666667 | 6.066667 |
8b0c03e92fcd51ffb191b89bc63aba66 | 1 | 0 | NaN | NaN | NaN | 0.000000 | NaN |
4f315989358cc0f3f7869f569887743d | 22 | 16 | 21.0 | 79.633333 | 387.316667 | 0.727273 | 3.200000 |
2cfe175b13f56f79e8e4b01eec4c8b2a | 19 | 16 | 19.0 | 36.316667 | 407.300000 | 0.842105 | 0.816667 |
278d8bf47d1ce3752fc5a856e14dd078 | 21 | 16 | 21.0 | 141.200000 | 131.300000 | 0.761905 | 1.616667 |
532 rows × 7 columns
Zcela základní ukazatale.
print("Počet studentů: ", len(ds))
print("Z nich kvíz splnilo: ", len(ds[ds.total_score >= POINTS_TO_PASS]))
Počet studentů: 532 Z nich kvíz splnilo: 462
Studenti s nejvíce otázkami.
ds.sort_values(by='total_questions', ascending=False).head(10)
total_questions | total_score | questions_to_success | minutes_to_success | minutes_left | success_rate | median_minutes_to_answer | |
---|---|---|---|---|---|---|---|
20314c01c1fdfe3bebe0a5c7c7bd1721 | 180 | 125 | 22.0 | 76.950000 | 71352.033333 | 0.088889 | 2.441667 |
ed855d7c23c5f8b9b9e0b8862ab2e402 | 136 | 16 | 135.0 | 55890.716667 | 12109.816667 | 0.139706 | 1.016667 |
d40567fd37806b2987eae21a7e36ecce | 87 | 32 | 27.0 | 9688.950000 | 61100.783333 | 0.183908 | 5.633333 |
5354ae126495667a24e77e66cc463276 | 82 | 16 | 81.0 | 4132.216667 | 1289.916667 | 0.195122 | 3.516667 |
3ba8c71ee1bc40e238090bf56adf5115 | 80 | 16 | 79.0 | 66160.650000 | 4518.350000 | 0.212500 | 1.616667 |
22fda4defe397ffa898278e4a1d9b796 | 79 | 16 | 79.0 | 17014.766667 | 821.516667 | 0.215190 | 3.850000 |
b1e7ba607de39c888c12b9dd4a546cac | 79 | 32 | 59.0 | 31870.700000 | 1678.666667 | 0.227848 | 2.483333 |
fe5fa747eb0928e45e7f237d7bc1224a | 78 | 16 | 77.0 | 9902.550000 | 58305.566667 | 0.243590 | 2.433333 |
0b8a5ff739b7d67831c6991fc14c9b1b | 75 | 32 | 25.0 | 410.650000 | 10485.400000 | 0.213333 | 2.783333 |
46c216bd1b64b3ced0e2a0614996f535 | 75 | 32 | 26.0 | 9428.166667 | 61175.700000 | 0.213333 | 3.975000 |
Kolik otázek studenti potřeboval ke splnění kvízu?
data = ds.questions_to_success
maxo = int(data.max())
ax = data.hist(bins=(maxo+1), figsize=(10, 5))
ax.set_xlabel('Počet otázek')
ax.set_ylabel('Počet studentů')
ax.set_title('Počet otázek potřebných ke splnění kvízu')
plt.show()
Kolik času (od otevření kvízu po jeho splnění) studentům řešení kvízu zabralo? Toto je hrubý nástřel, typicky bude obsahovat i noční dobu atp. I tak je ale výsledek zajímavý.
ax = ds.minutes_to_success.apply(lambda t: t / 60).hist(bins=100, figsize=(12, 5))
ax.set_xlabel('Čas v hodinách')
ax.set_ylabel('Počet studentů')
ax.set_title('Čas potřebný ke splnění kvízu')
plt.show()
Histogram úspěšnosti odpovídání.
ax = ds.success_rate.hist(bins=40, figsize=(10, 5))
ax.set_xlabel('Úspěšnost')
ax.set_ylabel('Počet studentů')
ax.set_title('Rozložení úspěšnosti odpovídání studentů')
plt.show()
Histogram mediánu času potřebného k odeslání odpovědi od vygenerování otázky.
ax = ds[ds.median_minutes_to_answer <= 60 * 6].median_minutes_to_answer.hist(bins=48, figsize=(10, 5))
ax.set_xlabel('Medián doby potřebné na zodpovězení (minuty)')
ax.set_ylabel('Počet studentů')
ax.set_title('Rozložení doby potřebné na zodpovězení')
plt.show()
Porovnání úspěšnosti odpovídání a času zbývajícího do deadline při splnění kvízu.
ax = ds.dropna(how='any', subset=['success_rate','minutes_left']).plot.hexbin(
x='success_rate', y='minutes_left', gridsize=25, figsize=(10,7), sharex=False
)
ax.set_xlabel('Úspěšnost')
ax.set_ylabel('Čas zbývající do deadline (minuty)')
ax.set_title('Úspěšnost vs. čas zbývající do deadline')
plt.show()
Porovnání úspěšnosti odpovídání a mediánu doby potřebné k odeslání jedné odpovědi.
ax = ds[ds.median_minutes_to_answer <= 15].dropna(how='any', subset=['success_rate','median_minutes_to_answer']).plot.hexbin(
x='success_rate', y='median_minutes_to_answer', gridsize=25, figsize=(10,7), sharex=False
)
ax.set_xlabel('Úspěšnost')
ax.set_ylabel('Medián doby na odpověď (minuty)')
ax.set_title('Úspěšnost vs. medián doby potřebné k zodpovězení')
plt.show()
Rozšíříme tabulku studentů o další sloupce.
for idx, row in ds.iterrows():
answers = df[df.anonymous_user_id == idx].copy()
if len(answers) <= 1:
continue
data = np.reshape(answers.to_deadline.values, (len(answers), 1))
clusters = fclusterdata(data, t=CLUSTER_THRESHOLD, criterion='distance')
answers['clusters'] = clusters
ds.at[idx, 'nclusters'] = np.max(clusters)
ds.nclusters = ds.nclusters.fillna(0).astype(np.int64)
Hrubý náhled.
ds
total_questions | total_score | questions_to_success | minutes_to_success | minutes_left | success_rate | median_minutes_to_answer | nclusters | |
---|---|---|---|---|---|---|---|---|
85eecf84d689d21fa27283a1a61be979 | 23 | 17 | 20.0 | 121.050000 | 71328.166667 | 0.695652 | 2.350000 | 3 |
bf9a2d34f2d553e01834cccc0ee29e01 | 34 | 16 | 34.0 | 3118.533333 | 68327.600000 | 0.470588 | 7.191667 | 6 |
283b9b9390a502d564eef1ef75c7b80d | 44 | 16 | 43.0 | 53262.366667 | 16480.750000 | 0.363636 | 7.300000 | 14 |
20314c01c1fdfe3bebe0a5c7c7bd1721 | 180 | 125 | 22.0 | 76.950000 | 71352.033333 | 0.088889 | 2.441667 | 38 |
94261009f7184d107bd03a9781a587c5 | 20 | 17 | 19.0 | 1647.550000 | 69781.283333 | 0.800000 | 4.266667 | 3 |
... | ... | ... | ... | ... | ... | ... | ... | ... |
b02def4f8d5e7bbdc0fac260a9ea3b44 | 24 | 16 | 23.0 | 377.650000 | 337.766667 | 0.666667 | 6.066667 | 2 |
8b0c03e92fcd51ffb191b89bc63aba66 | 1 | 0 | NaN | NaN | NaN | 0.000000 | NaN | 0 |
4f315989358cc0f3f7869f569887743d | 22 | 16 | 21.0 | 79.633333 | 387.316667 | 0.727273 | 3.200000 | 2 |
2cfe175b13f56f79e8e4b01eec4c8b2a | 19 | 16 | 19.0 | 36.316667 | 407.300000 | 0.842105 | 0.816667 | 1 |
278d8bf47d1ce3752fc5a856e14dd078 | 21 | 16 | 21.0 | 141.200000 | 131.300000 | 0.761905 | 1.616667 | 1 |
532 rows × 8 columns
Studenti nejvíce se vracející.
ds.sort_values(by='nclusters', ascending=False).head(10)
total_questions | total_score | questions_to_success | minutes_to_success | minutes_left | success_rate | median_minutes_to_answer | nclusters | |
---|---|---|---|---|---|---|---|---|
20314c01c1fdfe3bebe0a5c7c7bd1721 | 180 | 125 | 22.0 | 76.950000 | 71352.033333 | 0.088889 | 2.441667 | 38 |
3ba8c71ee1bc40e238090bf56adf5115 | 80 | 16 | 79.0 | 66160.650000 | 4518.350000 | 0.212500 | 1.616667 | 28 |
ed855d7c23c5f8b9b9e0b8862ab2e402 | 136 | 16 | 135.0 | 55890.716667 | 12109.816667 | 0.139706 | 1.016667 | 25 |
3a961aa46f31f0aac634e7c96bb5fb0e | 74 | 16 | 73.0 | 12820.366667 | 9228.000000 | 0.243243 | 1.983333 | 19 |
8b4a7f8ae7c1159a3a6210ba630181ca | 51 | 16 | 51.0 | 64691.550000 | 3517.100000 | 0.333333 | 0.950000 | 18 |
bc2cc9e892fdd0314e7bfe09bb1c6cc6 | 62 | 32 | 26.0 | 27501.983333 | 24912.750000 | 0.258065 | 10.058333 | 17 |
08afaa93bcd97eccfd0efe8f61cf894e | 39 | 16 | 39.0 | 33148.200000 | 30962.766667 | 0.410256 | 37.116667 | 16 |
5f54ce8a6e4bec16d4ea8dff0ab82aec | 42 | 16 | 42.0 | 53234.666667 | 17861.650000 | 0.380952 | 7.558333 | 15 |
513006c3cad910dbda9b1762f72aaeec | 66 | 16 | 66.0 | 56033.266667 | 322.250000 | 0.242424 | 1.166667 | 15 |
283b9b9390a502d564eef1ef75c7b80d | 44 | 16 | 43.0 | 53262.366667 | 16480.750000 | 0.363636 | 7.300000 | 14 |
data = ds.nclusters
maxc = data.max()
ax = data.hist(bins=maxc, figsize=(10, 5))
ax.set_xlabel('Počet "sezení"')
ax.set_ylabel('Počet studentů')
ax.set_title('Na kolik "sezení" studenti kvíz vyplnili')
plt.show()
Porovnání ve vztahu k úspěšnosti.
ax = ds[ds.median_minutes_to_answer <= 15].dropna(how='any', subset=['success_rate','nclusters']).plot.hexbin(
x='success_rate', y='nclusters', gridsize=25, figsize=(10,7), sharex=False
)
ax.set_xlabel('Úspěšnost')
ax.set_ylabel('Počet "sezení"')
ax.set_title('Úspěšnost vs. počet "sezení"')
plt.show()