# Výchozí parametry notebooku pro Papermill
FILENAME = 'bizma1.json'
START = '2020-09-24 20:00:00+0100'
DEADLINE = '2020-10-11 23:59:00+0100'
SCORE_TO_PASS = 15 # MARAST bodů
CLUSTER_THRESHOLD = 120 # minut
# Parameters
FILENAME = "bizma1.json"
START = "2020-09-24 20:00:00+0100"
DEADLINE = "2020-10-11 23:59:00+0100"
SCORE_TO_PASS = 15
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í počátku/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)
start = pd.to_datetime(START, utc=True)
deadline = pd.to_datetime(DEADLINE, utc=True)
Dopočtení dalších základních užitečných údajů:
Zahození dat, která pravděpodobně vznikla experimentováním nebo řešením singularit.
for idx, row in df.iterrows():
# doba od vytvoření otázky do zodpovězení (v minutách)
if row['answered_at']:
df.at[idx, 'duration'] = (row['answered_at'] - row['created_at']).seconds / 60
# doba od zodpovězení otázky do deadline (v minutách)
df.at[idx, 'to_deadline_answer'] = (deadline - row['answered_at']).total_seconds() / 60
# doba od vytvoření otázky do deadline (v minutách)
df.at[idx, 'to_deadline'] = (deadline - row['created_at']).total_seconds() / 60
df = df[(df.created_at >= start) & (df.created_at <= deadline)]
Souhrn sloupců, pro kontrolu.
df.columns
Přehled datových typů sloupců.
df.dtypes
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
Celkové počty studentů, otázek a příkladů.
print('Počet studentů: ', len(students))
print('Počet vygenerovaných otázek: ', len(df))
print('Počet zodpovězených otázek: ', len(df[~df.is_open]))
print('Počet příkladů: ', len(problem_ids))
print('Počet záporně ohodnocených: ', len(df[df.score < 0]))
Další "globální" statistiky.
print(
'Celková úspěšnost (správně zodpovězené / celkový počet odpovědí): ',
len(df[(~df.is_open) & (df.is_correctly_answered)]) / len(df[~df.is_open])
)
print('Průměrný počet minut potřebaný k odeslání odpovědi:', df.duration.mean())
print('Medián minut potřebaných k odeslání odpovědi:', df.duration.median())
Kdy jsou studenti nejaktivnější co se otevírání nových otázek, resp. odesílání odpovědí, a 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 vytvořených otázek')
ax.set_title('Otevírání nových otázek')
plt.xticks(range(25))
plt.show()
ax = df.answered_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 odeslaných odpovědí')
ax.set_title('Zodpovídání otázek')
plt.xticks(range(25))
plt.show()
Jak je vytváření otázek 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 vytvořených otázek')
ax.set_title('Aktivita studentů během doby přístupnosti kvízu')
plt.xticks(range(0, maxh, 24), rotation=90)
plt.show()
Jak dlouho studentům trvá na otázku odpovědět? Následující graf je ořezaný o několik outlierů (velmi dlouho otevřené otázky).
data = df[df.duration <= 10*df.duration.median()].duration
maxh = int(np.ceil(data.max()))
ax = data.hist(bins=maxh, figsize=(12, 5))
ax.set_xlabel('Počet minut na zodpovězení')
ax.set_ylabel('Počet otázek')
ax.set_title('Doba potřebaná k zodpovězení otázky')
plt.xticks(range(0, maxh, 1), rotation=90)
plt.show()
Předpočtení statistik jednotlivých příkladů.
# Nový DataFrame
dp = pd.DataFrame(index=problem_ids, columns=[
'displayed', 'kind', 'answers', 'correct', 'wrong', 'success_rate', 'median_time'
])
# Příklad po příkladu...
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. V posledním sloupci je uveden medián počtu minut potřebných na zodpovězení otázky.
with pd.option_context('display.max_rows', None, 'display.max_columns', None):
display(dp.sort_values(by=['success_rate']))
Závisí celková úspěšnost odpovídání na blízkosti deadline?
step = 6 * 60 # 3 hours
frame = pd.DataFrame(columns=['hours_to_deadline','success_ratio'])
for k in range(1, 8 * 7): # one week
dsnap = df[(df.to_deadline_answer >= (k-1)*step) & (k*step > df.to_deadline_answer)]
if len(dsnap) > 0:
frame.at[k, 'hours_to_deadline'] = k*step
frame.at[k, 'success_ratio'] = len(dsnap[dsnap.is_correctly_answered]) / len(dsnap)
maxt = frame.hours_to_deadline.max()
ax = frame.plot(
x="hours_to_deadline", y="success_ratio",
kind="bar", grid=True, legend=False
)
ax.set_xlabel('Počet minut na zodpovězení')
ax.set_ylabel('Počet otázek')
ax.set_title('Doba potřebaná k zodpovězení otázky')
plt.xticks(range(0, 8*7, 24), rotation=90)
plt.show()
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 >= SCORE_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()
ds.questions_to_success = ds.questions_to_success.fillna(0).astype(np.int64)
Kontrola datových typů.
ds = ds.infer_objects()
ds.dtypes
Náhled dat, z očividných důvodů nezobrazujeme všechny.
ds
Zcela základní ukazatale.
print("Počet studentů: ", len(ds))
print("Z nich kvíz splnilo: ", len(ds[ds.total_score >= SCORE_TO_PASS]))
Studenti s nejvíce otázkami.
ds.sort_values(by='total_questions', ascending=False).head(10)
Kolik otázek studenti potřeboval ke splnění kvízu?
data = ds[ds.questions_to_success > 0].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.xticks(range(0, maxo, SCORE_TO_PASS), rotation=90)
plt.show()
Další statistiky počtu otázek potřebných ke splnění kvízu.
print(
'Průměrný počet otázek potřebných ke splnění kvízu:',
ds[ds.questions_to_success > 0].questions_to_success.mean()
)
print(
'Medián počtu otázek potřebných ke splnění kvízu:',
ds[ds.questions_to_success > 0].questions_to_success.median()
)
print(
'Maximum počtu otázek potřebných ke splnění kvízu:',
ds[ds.questions_to_success > 0].questions_to_success.max()
)
print(
'Minimum počtu otázek potřebných ke splnění kvízu:',
ds[ds.questions_to_success > 0].questions_to_success.min()
)
Kolik času (od prvního 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ý.
data = ds[ds.minutes_to_success > 0].minutes_to_success.apply(lambda t: t / 60)
maxd = int(np.ceil(data.max()) / 2)
ax = data.hist(bins=maxd, figsize=(18, 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.xticks(range(0, 2*maxd, 4), rotation=90)
plt.show()
Kolik času studentům zbývalo do deadline v okamžik splnění kvízu?
data = ds[ds.minutes_to_success > 0].minutes_left.apply(lambda t: t / 60)
maxh = int(np.ceil(data.max()) / 2)
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 studentů')
ax.set_title('Kolik času zbývalo do deadline v okamžik splnění kvízu?')
plt.xticks(range(0, 2*maxh, 24), rotation=90)
plt.show()
Histogram úspěšnosti odpovídání.
ax = ds.success_rate.hist(bins=50, 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, pomocí clusterování se snažíme zjistit na kolik "sezení" student kvíz splnil.
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
Studenti nejvíce se vracející.
ds.sort_values(by='nclusters', ascending=False).head(10)
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()