Statistika průběžného kvízu

# 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

0. Načtení a předzpracování

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

1. Celkový přehled odpovědí, studentů a příkladů

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

2. Obtížnost a statistiky příkladů

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

3. Podrobnější analýza chování

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

4. Analýza počtu "sezení"

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