Statistiche gare Bebras italiano 2025

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 numpy as np
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')
CATEGORIES = tuple(f'{c}' for c in CATS)
CAT_FILES = tuple(f'{c}' 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_2025&events=0&test={127+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'] = 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 2025/26 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 2025/26 con risultati correttamente registrati
Categoria squadre min max media std.dev. I quartile mediana III quartile Squadre al minimo Squadre al massimo
kilo387404818.29.51118251.6%0.2%
mega1246604820.110.01320271.8%0.3%
giga596904821.710.41421290.8%0.6%
tera259304823.810.51624320.3%1.1%
peta195804819.47.81419240.7%0.1%
Totale26860
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))
No description has been provided for this image

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 (che percentuale di squadre si supera con un dato punteggio)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
1.62.93.64.86.79.010.713.116.719.322.126.630.834.038.442.746.449.654.057.661.064.968.471.674.277.779.882.584.586.888.690.492.193.294.695.796.597.297.798.398.898.999.299.599.599.899.899.8
Percentili per la categoria mega (che percentuale di squadre si supera con un dato punteggio)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
1.82.63.44.35.37.28.510.712.515.117.520.523.626.930.433.737.440.845.148.953.156.560.563.767.770.674.076.779.681.984.386.488.190.091.492.993.895.296.096.997.597.998.498.999.299.499.799.7
Percentili per la categoria giga (che percentuale di squadre si supera con un dato punteggio)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
0.81.31.62.43.44.45.87.49.411.614.116.519.622.926.530.234.037.841.545.448.952.655.959.563.066.069.071.974.576.979.281.583.485.186.888.890.691.893.293.894.595.796.697.497.998.298.699.4
Percentili per la categoria tera (che percentuale di squadre si supera con un dato punteggio)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
0.30.61.11.62.23.34.35.87.28.611.413.516.018.221.524.627.730.233.636.939.743.146.749.752.555.859.162.565.468.271.674.577.080.283.085.087.990.091.393.494.695.197.097.697.998.798.998.9
Percentili per la categoria peta (che percentuale di squadre si supera con un dato punteggio)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
0.70.71.11.73.04.05.46.58.310.513.115.619.422.226.429.935.739.645.551.056.461.166.871.776.179.283.286.288.690.092.494.095.196.196.997.398.398.698.798.999.599.599.799.899.999.999.999.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:
            errors += 1
print(errors)
2347
In [14]:
quiz = pd.DataFrame(rr)
In [15]:
quiz['q_id'] = quiz['q_id'].str.split(pat='_', expand=True)[1]
In [16]:
MAPNAMES = {
    'Q01': 'Il sonaglio',
    'Q02': 'Attraversare il fiume',
    'Q03': 'Spedizione di ricerca',
    'Q04': 'Bus',
    'Q05': 'Istruzioni per costruzioni',
    'Q06': 'Dalla foglia al legno',
    'Q07': 'Ragionamento spaziale',
    'Q08': 'Messaggi saltellanti',
    'Q09': 'Il magico giardino di Emma',
    'Q10': 'Piantare fiori',
    'Q11': "Bei disegni",
    'Q12': 'Genealogie',
    'Q13': "La corona dell'Avvento",
    'Q14': "L'età di Leone",
    'Q15': 'Robot',
    'Q16': 'Evitando le nuvole',
    'Q17': 'Call center per emergenze',
    'Q18': 'Coloranti biologici',
    'Q19': 'Una strada bloccata',
    'Q20': 'Ascensore',
    'Q21': 'La cerchia felice',
    'Q22': "L'ufficio cambi",
    'Q23': 'Il decollo',
    'Q24': 'Trasporto pubblico',
    'Q25': "La fila",
    'Q26': 'Giornata nebbiosa',
    'Q27': 'Parcheggio',
    'Q28': 'Robot pianta-fiori',
}

assert set(MAPNAMES.keys()) == set(quiz['q_id'].unique())
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']
quiz['edizione'] = '2025'
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'].astype('float64') / 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 np.nan)

quiz.to_csv('quiz.csv', columns=['anonid', 'cat', 'edizione', 'nome', 'score', 'score_max', 'time'])
In [19]:
vquiz = pd.merge(valid[['anonid', 'categoria', 'punteggio','punteggio_norm','orainizio','teacher_id','school_cap']], quiz, on='anonid')
In [28]:
fig, ax = plt.subplots(ncols=1, nrows=len(valid['categoria'].unique()), figsize=(16, 40))
for j, k in enumerate(valid['categoria'].unique().sort_values()):
    ax[j].set_ylim([0, 1])
    qq = vquiz[vquiz['categoria'] == k]
    m = qq.groupby('nome', sort=False)[['completo','voto', 'parziale', 'penalizzato', 'minuti','score_max']].mean()
    m['vparziale'] = m['voto'] - m['completo']
    c = ax[j].bar(np.arange(m.index.size), m['completo'], color='blue')
    p = ax[j].bar(np.arange(m.index.size), m['parziale'], bottom=m['completo'], color='lightblue')
    ax[j].set_xticks(np.arange(m.index.size), map(lambda x: MAPNAMES[x], m.index.tolist()), rotation=45)
    ax[j].set_yticks(np.arange(0,1.2,.2), [f'{100*y:.0f}%' for y in np.arange(0,1.2,.2)])
    for i, y in enumerate(m['voto'].tolist()):
        ax[j].annotate(text=f"{m['minuti'].iloc[i]:.0f}'", xy=(i, .75*m['completo'].iloc[i]), color='white')
        ax[j].annotate(text=f"{m['score_max'].iloc[i]:.0f}", xy=(i-.15, .02), color='yellow', fontsize='x-large')
    ax[j].legend((c[0],p[0]), ('completo','parziale'), loc=(.92,.6))
    ax[j].set_title(f'{k}: tassi di soluzione (il numero in alto indica i minuti spesi in media sul quesito, \
il numero in basso il punteggio massimo ottenibile)')

fig.tight_layout()
fig.savefig('tassisol.png')
No description has been provided for this image
In [35]:
fig, ax = plt.subplots(ncols=1, nrows=len(valid['categoria'].unique()), figsize=(16, 40))
for j, k in enumerate(valid['categoria'].unique().sort_values()):
    ax[j].set_ylim([-1, 1])
    qq = vquiz[vquiz['categoria'] == k]
    m = qq.groupby('nome', sort=False)[['completo','voto', 'parziale', 'penalizzato', 'minuti','score_max']].mean()
    m['vparziale'] = m['voto'] - m['completo']

    c = ax[j].bar(np.arange(m.index.size), m['completo'] + m['parziale'], color='green')
    z = ax[j].bar(np.arange(m.index.size), -m['penalizzato'], color='red')
    ax[j].set_yticks(np.arange(-1,1.2,.2), [f'{100*abs(y):.0f}%' for y in np.arange(-1,1.2,.2)])
 
    ax[j].set_xticks(np.arange(m.index.size), map(lambda x: MAPNAMES[x], m.index.tolist()), rotation=45)
    for i, y in enumerate(m['voto'].tolist()):
        ax[j].annotate(text=f'{m['score_max'].iloc[i]:.0f}', xy=(i, -.8), color='blue')
    
    ax[j].legend((c[0],z[0]), ('punteggio pieno/parziale','penalità'), loc=(0.91,.725))
    ax[j].set_title(f'{k}: percentuale di punteggio attribuito in media, in rosso la percentuale di penalizzati (il numero in basso è il punteggio massimo)')

fig.tight_layout()
fig.savefig('punti.png')
No description has been provided for this image

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['anonid'] = hashlib.md5(str(r.team_id).encode('utf8')).hexdigest()
            members.append(m)

pupils = pd.DataFrame(members)
pupils['genere'] = pupils['sex'].map(lambda x: x if x != '-' else np.nan)
pupils['categoria'] = pupils['categoria'].astype(pd.api.types.CategoricalDtype(categories = CATEGORIES, ordered=True))
In [23]:
gender = pupils[pupils['genere'].notnull()].groupby(['categoria', 'genere']).count()
txt = '''<table><caption>Studenti partecipanti al Bebras 2025 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'].isin(['m', 'f'])].groupby('categoria')['anonid'].nunique()
empty = pupils[pupils['genere'].isnull()].groupby('categoria')['anonid'].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 2024 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
kilo80153832 (47.8%)4183 (52.2%)13412.69
mega2472711996 (48.5%)12731 (51.5%)41152.73
giga118595712 (48.2%)6147 (51.8%)19542.72
tera50561523 (30.1%)3533 (69.9%)10632.63
peta43171074 (24.9%)3243 (75.1%)6032.65
Totale:5397424137 (44.7%)29837 (55.3%)
Totale comprese squadre con dati mancanti ≧ 63050
In [24]:
compositions = pupils[pupils['genere'].isin(['m', 'f'])].groupby('anonid')['genere'].agg(lambda xx: ''.join(sorted(xx.sum())))

gquiz = pd.merge(vquiz, compositions, on='anonid')

gquiz['n_m'] = gquiz['genere'].str.count('m')
gquiz['n_f'] = gquiz['genere'].str.count('f')
gquiz['n'] = gquiz['n_m'] + gquiz['n_f']

gquiz.to_csv('mf.csv')

I nomi delle squadre più comuni

In [25]:
import re
from collections import Counter

notwanted = re.compile('^0[0-9]+$|^[0-9][a-zA-Z0-9_]|^the$|^and$|^classe$|^squadra$|^gruppo$|^team$|^i+$|^iv$|^[a-zA-Z0-9_]$|^prima$|^seconda$\
|^terza$|^quarta$|^quinta$|^squadra|^$')

names = scoredf['team_name'].str.strip().str.lower().tolist()
oknames = filter(lambda w: not notwanted.match(w), names)

c = Counter(oknames)

c.most_common(30)
Out[25]:
[('i matematici', 35),
 ('blu', 22),
 ('le girls', 22),
 ('i leoni', 20),
 ('i tre moschettieri', 20),
 ('gli informatici', 19),
 ('viola', 18),
 ('mercurio', 17),
 ('rosso', 17),
 ('giove', 17),
 ('marte', 16),
 ('venere', 16),
 ('verde', 16),
 ('leoni', 16),
 ('le volpi', 16),
 ('le tigri', 16),
 ('giallo', 15),
 ('le stelle', 15),
 ('le winx', 15),
 ('il triumvirato', 15),
 ('nettuno', 14),
 ('i pinguini', 13),
 ('urano', 13),
 ('terra', 13),
 ('saturno', 13),
 ('boh', 12),
 ('sole', 11),
 ('i campioni', 11),
 ('le calcolatrici', 11),
 ('le baddie', 11)]
In [26]:
plt.axis('off')
os = scoredf['operating_system'].value_counts().plot.pie(autopct='%.1f', radius=1.22,
                                                    explode=[.06*i*i for i in range(len(scoredf['operating_system'].unique()))],
                                                    figsize=(5,5), title='Sistemi operativi utilizzati')
No description has been provided for this image
In [ ]: