Statistiche gare Bebras italiano 2021

In [1]:
from IPython.display import HTML

HTML('''<script>
code_show=true; 
function code_toggle() {
 if (code_show){
 $('div.input').hide();
 } else {
 $('div.input').show();
 }
 code_show = !code_show
} 
$( document ).ready(code_toggle);
</script>
<input type="button" value="Clicca per vedere/nascondere il codice Python" onclick="code_toggle()">''')
Out[1]:
In [2]:
import warnings
#warnings.filterwarnings('once')
warnings.filterwarnings('ignore')

Distribuzione dei punteggi

In [3]:
import pandas as pd
import json, hashlib, urllib, os.path

pd.options.display.max_rows = None
pd.options.display.max_columns = None
In [4]:
CATS = ('kilo','mega','giga','tera','peta')
SUBS = ('single', 'double')
CATEGORIES = tuple(f'{c}-{s}' for c in CATS for s in SUBS)
CAT_FILES = tuple(f'{c}-{s}' for s in SUBS for c in CATS)
In [5]:
with open('secret.key') as k:
    key = k.readline().strip()

for i, k in enumerate(CAT_FILES):
    if not os.path.exists(f"results-{k}.json"):
        r = urllib.request.urlopen(f"https://bebras.it/api?key={key}&view=exams&edition=bebras_2021&events=0&test={98+i}")
        with open(f"results-{k}.json", "w") as tw:
            tw.writelines(r.read().decode('utf-8'))
In [6]:
score = []
for k in CATEGORIES:
    with open(f"results-{k}.json", "r") as t:
        j = json.load(t)
        score += j['exams']
In [7]:
scoredf = pd.DataFrame(score)
In [8]:
# L'orario va corretto per il fuso orario

scoredf['server_start'] = pd.to_datetime(scoredf['exam_date'].astype('int64') + 60*60, unit='s')
scoredf['orainizio'] = pd.np.floor((scoredf['exam_date'].astype('int64') + 60*60) / (45*60)) # ore da 45', il tempo di gara
scoredf['punteggio'] = pd.to_numeric(scoredf['score'])
scoredf['punteggio_norm'] = scoredf['punteggio'].map(lambda x: x if x >= 0 else 0)
scoredf['anonid'] = scoredf['team_id'].map(lambda x: hashlib.md5(str(x).encode('utf8')).hexdigest())
scoredf['categoria'] = scoredf['category'].str.lower().astype(pd.api.types.CategoricalDtype(categories = CATEGORIES, ordered=True))
In [9]:
valid = scoredf[scoredf['exam_valid_score'] == 1]
valid.to_csv('anonris.csv', columns=['anonid', 'categoria', 'orainizio', 'punteggio', 'punteggio_norm', 'time'])
In [10]:
from IPython.display import display, Markdown

txt = '''<table>
<caption>Squadre partecipanti al Bebras 2021/22 con risultati correttamente registrati</caption>
<thead>
  <tr><th>Categoria</th>
  <th>squadre</th>
  <th> min </th>
  <th> max </th>
  <th> media </th>
  <th> std.dev. </th>
  <th>I quartile </th>
  <th>mediana </th>
  <th>III quartile</th>
  <th>Squadre al minimo</th>
  <th>Squadre al massimo</th>
</tr>
<tbody>
'''
for k in valid['categoria'].unique().sort_values():
    s = valid[valid['categoria'] == k]['punteggio_norm'].describe()
    top = valid[(valid['categoria'] == k) & (valid['punteggio_norm'] == int(s['max']))]
    bottom = valid[(valid['categoria'] == k) & (valid['punteggio_norm'] == int(s['min']))]
    txt += "<tr><th>{}</th><td>{}</td><td>{}</td><td>{}</td><td>{:.1f}</td>\
<td>{:3.1f}</td><td>{}</td><td>{}</td><td>{}</td><td>{:.1f}%</td><td>{:.1f}%</td></tr>".format(k, 
                                                              int(s['count']),
                                                              int(s['min']),
                                                              int(s['max']),
                                                              float(s['mean']),
                                                              float(s['std']),
                                                              int(s['25%']), 
                                                              int(s['50%']), 
                                                              int(s['75%']),
                                                              100*len(bottom)/float(s['count']),
                                                              100*len(top)/float(s['count']))
txt += '<tfoot><tr><th>Totale</th><td>{}</td></tr>'.format(valid['punteggio_norm'].count())
txt += '</table>'
display(Markdown(txt))
Squadre partecipanti al Bebras 2021/22 con risultati correttamente registrati
Categoria squadre min max media std.dev. I quartile mediana III quartile Squadre al minimo Squadre al massimo
kilo-single163804815.210.3714226.3%0.4%
kilo-double182804818.99.91218252.2%0.5%
mega-single729404812.59.65111810.1%0.2%
mega-double428104814.710.0713216.5%0.2%
giga-single33020469.98.2381512.1%0.0%
giga-double204204811.48.75101711.1%0.1%
tera-single303704814.99.0814214.0%0.0%
tera-double160304514.99.0814214.8%0.1%
peta-single161705020.310.61219281.9%0.1%
peta-double141105021.010.31321281.3%0.1%
Totale28053
In [11]:
%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('ggplot')

histograms = valid['punteggio_norm'].hist(by=valid['categoria'], bins=24, figsize=(10,16), layout=(5, 2))

Percentili per punteggio

In [12]:
for k in valid['categoria'].unique().sort_values():
    tot = float(valid[(valid['categoria'] == k)]['punteggio'].count())
    top = int(valid[(valid['categoria'] == k)]['punteggio'].max())
    pp = [100 * valid[(valid['categoria'] == k) & (valid['punteggio'] < i)]['punteggio'].count()/tot for i in range(1,top+1)]
    txt = '''<table>
    <caption>Percentili per la categoria {} (che percentuale di squadre si supera con un dato punteggio)</caption>
    <thead>'''.format(k)
    txt += ''.join(['<td>{}</td>'.format(i) for i in range(1,top+1)])
    txt += '<tbody>'
    txt += ''.join(['<td>{:.1f}</td>'.format(f) for f in pp])
    txt += '</table>'
    display(Markdown(txt))    
Percentili per la categoria kilo-single (che percentuale di squadre si supera con un dato punteggio)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
6.38.510.613.216.519.222.725.629.932.436.039.943.547.850.654.657.962.065.268.971.674.177.480.182.783.885.586.388.289.590.792.293.093.894.996.497.097.597.998.398.499.099.299.399.599.599.699.6
Percentili per la categoria kilo-double (che percentuale di squadre si supera con un dato punteggio)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
2.23.24.35.26.77.910.812.314.817.319.923.627.431.233.838.542.747.551.355.959.263.567.069.973.975.678.780.782.985.387.288.890.091.192.593.994.695.496.096.697.198.699.099.199.399.499.599.5
Percentili per la categoria mega-single (che percentuale di squadre si supera con un dato punteggio)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
10.113.116.719.823.026.931.135.640.344.648.652.356.059.863.866.969.973.276.078.380.782.884.786.287.989.590.591.792.893.894.795.496.096.697.197.698.198.598.798.898.999.199.699.799.899.899.899.8
Percentili per la categoria mega-double (che percentuale di squadre si supera con un dato punteggio)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
6.58.510.913.416.619.723.126.931.135.338.842.846.550.053.657.761.064.266.970.073.476.078.580.582.584.686.388.089.390.692.193.494.395.195.796.797.398.098.398.498.798.899.699.799.899.899.899.8
Percentili per la categoria giga-single (che percentuale di squadre si supera con un dato punteggio)
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
12.116.721.826.330.336.141.046.251.655.860.364.468.071.474.477.279.982.384.286.187.989.690.892.093.294.695.796.697.397.898.598.599.099.299.399.599.799.799.899.899.899.999.999.999.9100.0
Percentili per la categoria giga-double (che percentuale di squadre si supera con un dato punteggio)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
11.114.318.221.424.429.233.638.142.046.351.356.260.264.068.070.874.177.279.582.084.486.388.090.091.292.293.694.895.496.397.197.698.698.798.999.399.599.699.899.899.899.899.999.999.999.999.999.9
Percentili per la categoria tera-single (che percentuale di squadre si supera con un dato punteggio)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
4.05.26.58.512.614.119.323.226.930.634.739.244.448.252.256.460.364.167.670.773.977.079.582.284.686.688.790.591.792.994.295.296.296.897.898.298.698.899.299.499.799.899.999.999.999.9100.0100.0
Percentili per la categoria tera-double (che percentuale di squadre si supera con un dato punteggio)
123456789101112131415161718192021222324252627282930313233343536373839404142434445
4.86.27.89.513.315.019.722.125.529.133.437.943.347.351.256.160.964.868.170.974.176.779.782.384.987.088.990.692.193.494.695.195.997.097.998.498.798.899.199.399.599.599.899.999.9
Percentili per la categoria peta-single (che percentuale di squadre si supera con un dato punteggio)
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
1.92.33.54.15.37.28.510.913.916.818.922.826.128.331.536.238.942.146.350.554.158.060.463.266.869.972.475.078.680.482.784.585.787.388.990.792.093.294.294.796.096.797.698.498.698.799.099.399.899.9
Percentili per la categoria peta-double (che percentuale di squadre si supera con un dato punteggio)
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
1.31.72.32.73.95.26.99.010.813.616.919.723.226.329.933.536.541.444.446.849.954.157.661.264.668.170.573.476.378.781.583.385.186.388.790.491.892.894.094.795.896.797.798.499.099.299.699.699.899.9

Analisi delle risposte

In [13]:
rr = []
errors = 0
for r in valid.itertuples():
    for q in r.exam_data['questions']:
        try:
            t = dict((k, q[k]) for k in ('q_id','q_class','q_score','q_scoreMax','q_time'))
            t['anonid'] = r.anonid
            rr.append(t)
        except Exception as e:
            #print(q, e)
            errors += 1
print(errors)
6043
In [14]:
quiz = pd.DataFrame(rr)
In [15]:
MAPBEBRAS = dict((x.split('_')[-1], x.split('_')[1]) for x in list(quiz['q_id'].unique()))
In [16]:
MAPNAMES = {
    'Q01': 'Lettere',
    'Q02': 'Vestito da ballo',
    'Q03': 'Assistente virtuale',
    'Q04': 'Timbri',
    'Q05': 'Mappa concettuale',
    'Q06': 'Sequenze di DNA',
    'Q07': 'Collane',
    'Q08': 'Display difettoso',
    'Q09': 'Fotografie del gatto',
    'Q10': 'Case nel villaggio',
    'Q11': 'Auto con guida autonoma',
    'Q12': 'Un gioco coi birilli',
    'Q13': "Ricostruire il DNA",
    'Q14': "Cuculi e nidi",
    'Q15': 'Topo-robot',
    'Q16': 'Canguri',
    'Q17': 'I gruppi di lavoro',
    'Q18': 'La pila di frutta',
    'Q19': 'Logistica museale',
    'Q20': 'Cenni del capo',
    'Q21': 'Disegni programmati',
    'Q22': 'Un codice compresso',
    'Q23': 'Rilevamento di guasti',
    'Q24': 'Andiamo in biblioteca',
    'Q25': "Di corsa all'incontro",
    'Q26': 'Salviamo gli alberi',
    'Q27': 'Piastrelle Truchet',
    'Q28': "Ada l'ingegnera",
}
In [17]:
quiz = quiz.rename(columns={'q_time': 'time', 'q_score': 'score', 'q_scoreMax': 'score_max', 'q_class': 'cat'})
In [18]:
quiz['nome'] = quiz['q_id'].str.extract('[0-9]+_(.+)', expand=False)
quiz['edizione'] = quiz['q_id'].str.extract('([0-9]+)_.+', expand=False)
quiz['completo'] = quiz['score'] == quiz['score_max']
quiz['parziale'] = (quiz['score'] > 0) & (quiz['score'] != quiz['score_max'])
quiz['penalizzato'] = quiz['score'] < 0
quiz['voto'] = quiz['score'] / quiz['score_max'].astype('float64')
quiz['minuti'] = quiz['time'].map(lambda x: float(x)/60. if float(x) >= 0 and float(x) <= 45*60 else pd.np.NaN)

#quiz.to_csv('quiz.csv', columns=['anonid', 'cat', 'edizione', 'nome', 'bebras', 'score', 'score_max', 'time'])
In [19]:
vquiz = pd.merge(valid[['anonid', 'categoria', 'punteggio','punteggio_norm','orainizio','teacher_id','school_cap']], quiz, on='anonid')
In [20]:
plt.figure(figsize=(16,40))

def bname(n):
    if n in MAPBEBRAS and n in MAPNAMES:
        return '{}'.format(MAPNAMES[n])
    else:
        return n

for j, k in enumerate(valid['categoria'].unique().sort_values()):
    plt.subplot(len(valid['categoria'].unique()), 1, j+1)
    plt.ylim(0,1.2)
    m = vquiz[vquiz['categoria'] == k].groupby('nome', 
                                             sort=False)[['completo','voto', 'parziale', 'penalizzato', 'minuti','score_max']].mean()
    m['vparziale'] = m['voto'] - m['completo']

    c = plt.bar(pd.np.arange(m.index.size), m['completo'], color='blue')
    p = plt.bar(pd.np.arange(m.index.size), m['parziale'], bottom=m['completo'], color='lightblue')   
    plt.xticks(pd.np.arange(m.index.size), map(bname, m.index.tolist()), rotation=45)
    plt.ylim([0,1])
    plt.yticks(pd.np.arange(0,1.2,.2), ['{:.0f}%'.format(100*y) for y in pd.np.arange(0,1.2,.2)])
    for i, y in enumerate(m['voto'].tolist()):
        plt.annotate(text='{:.0f}\''.format(m['minuti'].iloc[i]), xy=(i, .75*m['completo'].iloc[i]), color='white')
        plt.annotate(text='{:.0f}'.format(m['score_max'].iloc[i]), xy=(i-.15, .02), color='yellow', fontsize='x-large')
    plt.legend((c[0],p[0]), ('completo','parziale'), loc=(.92,.6))
    plt.title('{}: tassi di soluzione (il numero in alto indica i minuti spesi in media sul quesito, \
il numero in basso il punteggio massimo ottenibile)'.format(k))

plt.tight_layout()
plt.savefig('tassisol.png')
In [21]:
plt.figure(figsize=(16,40))

for j, k in enumerate(valid['categoria'].unique()):
    plt.subplot(len(valid['categoria'].unique()),1, j+1)
    plt.ylim(0,1.2)
    m = vquiz[vquiz['categoria'] == k].groupby('nome', 
                                             sort=False)[['completo','voto', 'parziale', 'penalizzato', 'minuti','score_max']].mean()
    m['vparziale'] = m['voto'] - m['completo']

    c = plt.bar(pd.np.arange(m.index.size), m['voto'], color='green')
    z = plt.bar(pd.np.arange(m.index.size), -m['penalizzato'], color='red')
    plt.ylim([-1,1])
    plt.yticks(pd.np.arange(-1,1.2,.2), ['{:.0f}%'.format(100*abs(y)) for y in pd.np.arange(-1,1.2,.2)])
 
    plt.xticks(pd.np.arange(m.index.size), map(bname, m.index.tolist()), rotation=45)
    for i, y in enumerate(m['voto'].tolist()):
        plt.annotate(text='{:.0f}'.format(m['score_max'].iloc[i]), xy=(i, -.8), color='blue')
    
    plt.legend((c[0],z[0]), ('punteggio','penalità'), loc=(0.91,.725))
    plt.title('{}: percentuale di punteggio attribuito in media, in rosso la percentuale di penalizzati (il numero in basso è il punteggio massimo)'.format(k))

plt.tight_layout()
plt.savefig('punti.png')

Analisi delle squadre

In [22]:
members = []
for r in valid.itertuples():
    if r.team_composition and 'members' in r.team_composition:
        for m in r.team_composition['members']:
            m['categoria'] = r.category.lower()
            m['team_id'] = r.team_id
            members.append(m)

pupils = pd.DataFrame(members)
pupils['genere'] = pupils['sex'].map(lambda x: x if x != '-' else pd.np.NaN)
pupils['categoria'] = pupils['categoria'].astype(pd.api.types.CategoricalDtype(categories = CATEGORIES, ordered=True))
In [29]:
gender = pupils[pupils['genere'].notnull()].groupby(['categoria', 'genere']).count()
txt = '''<table><caption>Studenti partecipanti al Bebras 2021 con risultati validi 
(i dati dipendono dalla corretta compilazione dei profili delle squadre)</caption>
<thead>
  <tr><th>Categoria</th>
  <th>studenti</th>
  <th>femmine</th>
  <th>maschi</th>
  <th>squadre con dati mancanti</th>
  <th>media componenti per squadra</th>
  </tr>
<tbody>
'''
notempty = pupils[pupils['genere'].notnull()].groupby('categoria')['team_id'].nunique()
empty = pupils[pupils['genere'].isnull()].groupby('categoria')['team_id'].nunique()

totf = 0
totm = 0
tot = 0
for k in pupils['categoria'].unique().sort_values():
    f = gender.loc[(k,'f')]['class']
    totf += f
    m = gender.loc[(k,'m')]['class']
    totm += m
    tot += f + m + empty[k]*(2 if '-double' in k else 1)
    
    txt += f'<tr><th>{k}</th><td>{f+m}</td><td>{f} ({100*float(f)/float(f+m):.1f}%)</td><td>{m} ({100*float(m)/float(f+m):.1f}%)</td><td>{empty[k]}</td><td>{float(f+m) / float(notempty[k]):.2f}</td></tr>'

txt += f'<tr><th>Totale:</th><td>{totf+totm}</td><td>{totf} ({100*float(totf)/float(totf+totm):.1f}%)</td><td>{totm} ({100*float(totm)/float(totf+totm):.1f}%)</td></tr>'
txt += f'<tr><th>Totale comprese squadre con dati mancanti</th><td>{tot}</td></tr>'
txt += '</table>'
display(Markdown(txt)) 
Studenti partecipanti al Bebras 2021 con risultati validi (i dati dipendono dalla corretta compilazione dei profili delle squadre)
Categoria studenti femmine maschi squadre con dati mancanti media componenti per squadra
kilo-single1370674 (49.2%)696 (50.8%)14810.92
kilo-double31181571 (50.4%)1547 (49.6%)1501.83
mega-single52272511 (48.0%)2716 (52.0%)61600.88
mega-double61472902 (47.2%)3245 (52.8%)7121.67
giga-single24531184 (48.3%)1269 (51.7%)27240.90
giga-double30221469 (48.6%)1553 (51.4%)3311.68
tera-single2182615 (28.2%)1567 (71.8%)28710.90
tera-double2523761 (30.2%)1762 (69.8%)2641.68
peta-single1283292 (22.8%)991 (77.2%)13300.96
peta-double2316619 (26.7%)1697 (73.3%)1981.72
Totale:2964112598 (42.5%)17043 (57.5%)
Totale comprese squadre con dati mancanti47517