Grafote: ¿Cuáles son mis notas menos confiables?
Contents
Grafote: ¿Cuáles son mis notas menos confiables?#
Este grafo se encarga de analizar las notas asignadas por parte de las materias hacia estudiantes con el objetivo de identificar las materias injustas, notas confiables, mejores estudiantes, y más. Para esto se utiliza el algoritmo REV2.
REV2 tiene como objetivo encontrar usuarios fraudulentos en plataformas que tengan ratings hacia ciertos productos.
Bajo el contexto de FIUBA, podríamos pensar a las materias como “usuarios”, a los estudiantes como “productos”, y las notas de final como “calificaciones” con el objetivo de aplicar REV2 con algunos supuestos:
Un estudiante mantiene un cierto desempeño desde su inicio a fin de carrera.
La forma de evaluar de cada materia se mantiene consistente más allá de cátedras o profesores.
No esperamos encontrar materias fraudulentas, pero de cualquier forma podemos analizar los valores F, G y R para materias, usuarios y notas que devuelve el algoritmo.
¿Cómo es el grafo?#
El grafo va a ser bipartito
Nodos: Materias por un lado, estudiantes por el otro
Aristas: Un estudiante aprobó una materia, la arista representa la nota y está direccionada de la materia hacia el estudiante (materia valoró a este estudiante con esta nota)
Peso: Nota de final
A diferencia de los otros grafos, este utiliza la información de todas las carreras en vez de limitarse a solo una.
Preprocesamiento#
import networkx as nx
import pandas as pd
import numpy as np
import scipy as sp
from tqdm.notebook import tqdm
import utils
Obtenemos todos los planes de carrera de FIUBA, para tener los nombres de materias al analizar los resultados
df_planes = []
for plan in utils.PLANES:
df_plan = pd.read_json(utils.plan_estudios(plan))
df_plan['Carrera'] = plan
df_planes.append(df_plan)
df_planes = pd.concat(df_planes)
df_planes = df_planes.rename(columns={'materia': 'materia_nombre', 'id': 'materia_id'})
df_planes.sample(3)
materia_id | materia_nombre | creditos | categoria | level | correlativas | requiere | requiereCBC | Carrera | |
---|---|---|---|---|---|---|---|---|---|
68 | 92.12 | Industrias Textiles | 4 | Materias Electivas | NaN | 91.12-92.01 | NaN | NaN | industrial |
80 | 86.56 | Procesamiento de Imágenes | 6 | Multiples Orientaciones | NaN | 86.51-86.55 | NaN | NaN | electronica |
28 | 97.01 | Higiene y Seguridad Industrial | 4 | Materias Obligatorias | 5.0 | NaN | 118.0 | 1.0 | industrial |
Y el grafo con las notas en sí
df = pd.read_pickle('fiuba-map-data-all.pickle')
display(df.sample(3))
df.shape
Padron | Carrera | Orientacion | Final de Carrera | materia_id | materia_nota | materia_cuatrimestre | checkboxes | optativas | aplazos | |
---|---|---|---|---|---|---|---|---|---|---|
97671 | 98133 | industrial | NaN | tpp | 82.01 | 4.0 | NaN | NaN | NaN | NaN |
15692 | 100566 | informatica | Sistemas Distribuidos | tpp | 75.52 | -2.0 | 2024.0 | NaN | NaN | NaN |
76249 | 102429 | industrial | NaN | tpp | 81.01 | 8.0 | 2017.0 | NaN | NaN | NaN |
(104335, 10)
# join con los planes, para tenerlo
df = pd.merge(df, df_planes, on=['materia_id', 'Carrera'])
# filtros de columnas relevantes
df = df[['Padron', 'materia_id', 'materia_nota', 'materia_nombre', 'Carrera']]
df.head()
Padron | materia_id | materia_nota | materia_nombre | Carrera | |
---|---|---|---|---|---|
0 | Mica | CBC | 0.0 | Ciclo Básico Común | quimica |
1 | 106023 | CBC | 0.0 | Ciclo Básico Común | quimica |
2 | 103928 | CBC | 0.0 | Ciclo Básico Común | quimica |
3 | 103413 | CBC | 0.0 | Ciclo Básico Común | quimica |
4 | 106253 | CBC | 0.0 | Ciclo Básico Común | quimica |
# Se toman solo cursadas aprobadas
df_notas = df[df['materia_nota'] >= 4].copy()
# Se ignora el CBC
df_notas = df_notas[~df_notas['materia_id'].str.contains('CBC')]
df_notas['nota_mediana'] = df_notas.groupby(['Padron', 'Carrera'])['materia_nota'].transform('median')
df_notas['nota_promedio'] = df_notas.groupby(['Padron', 'Carrera'])['materia_nota'].transform('mean')
df_notas['nota_diferentes'] = df_notas.groupby(['Padron', 'Carrera'])['materia_nota'].transform('nunique')
# Para reducir ruido, se eliminan mapas donde las notas son casi "todas iguales"
df_notas = df_notas[df_notas['nota_diferentes'] >= 4]
df_notas = df_notas[df_notas['nota_mediana'] > 4.5]
df_notas.shape
(44391, 8)
# Normalizar notas de [-1 a 1]
df_notas['materia_nota_norm'] = (df_notas['materia_nota'] - 7) / 3
df_notas['materia_nota_norm'].value_counts()
0.000000 9846
0.333333 9490
-0.333333 7402
0.666667 6267
-0.666667 4507
-1.000000 3993
1.000000 2886
Name: materia_nota_norm, dtype: int64
# Se agregan columnas de cantidades, para luego filtrar más adelante
df_notas['cant_materias'] = df_notas.groupby(['Padron'])['materia_id'].transform('count')
df_notas['cant_padrones'] = df_notas.groupby(['materia_id'])['Padron'].transform('count')
Algoritmo REV2#
Se plantea el algoritmo genérico de REV2 para luego aplicarlo al grafo armado.
def rev2(
df,
col_in,
col_out,
col_rating,
gamma_1=0.5,
gamma_2=0.5,
n_iter=20,
):
"""Algoritmo REV2. Dado un dataframe con entradas, salidas y ratings
denotados por los nombres de columna, devuelve series para aplicar los
valores de F, G y R.
"""
assert df[col_rating].min() == -1 and df[col_rating].max() == 1, "Rating no fue normalizado a [-1, 1]"
ratings = nx.from_pandas_edgelist(
df,
create_using=nx.DiGraph,
source=col_in,
target=col_out,
edge_attr=[col_rating]
)
F = {}
G = {}
for val_in, val_out in tqdm(ratings.edges(), 'Inicializando estructuras'):
F[val_in] = 1
G[val_out] = 1
ratings[val_in][val_out]['R'] = 1
for i in tqdm(range(n_iter), 'Realizando iteraciones F/G/R'):
for val_out in G:
s = 0
n = 0
for val_in in ratings.predecessors(val_out):
s += ratings[val_in][val_out]['R'] * ratings[val_in][val_out][col_rating]
n += 1
G[val_out] = s / n
assert -1 <= G[val_out] <= 1
for val_in, val_out in ratings.edges():
R_new = (gamma_1 * F[val_in] + gamma_2 * (1 - (abs(ratings[val_in][val_out][col_rating] - G[val_out]) / 2))) / (gamma_1 + gamma_2)
ratings[val_in][val_out]['R'] = R_new
assert 0 <= R_new <= 1
for val_in in F:
s = 0
for val_out in ratings[val_in]:
s += ratings[val_in][val_out]['R']
F[val_in] = s / len(ratings[val_in])
assert 0 <= F[val_in] <= 1
series_F = df[col_in].apply(lambda x: F[x])
series_G = df[col_out].apply(lambda x: G[x])
series_R = df[[col_in, col_out]].apply(lambda x: ratings[x[col_in]][x[col_out]]['R'], axis=1)
return series_F, series_G, series_R, ratings
series_F, series_G, series_R, G = rev2(
df_notas,
'materia_id',
'Padron',
'materia_nota_norm',
)
df_notas['F'] = series_F
df_notas['G'] = series_G
df_notas['R'] = series_R
# Veamos (un sample de) el grafo sobre el cual se corrió REV2
import matplotlib.pyplot as plt
import utils
from config import PADRON
import random
# Subgrafo => 3 materias troncales y 3 padrones al azar
materias = ["62.01", "61.03", "61.08"]
random_edges = random.choices(list(filter(lambda e: any([m in e for m in materias]), G.edges)), k=3)
random_padrones = [p for m, p in random_edges]
nodes = [*random_padrones, *materias]
subgraph = nx.subgraph(G, nodes)
plt.figure(figsize=(15,5))
plt.title(f"Sample of a {G}")
nx.draw_networkx(subgraph,
pos=nx.bipartite_layout(subgraph, nx.bipartite.sets(subgraph)[0]))

Análisis de resultados#
Para el análisis de materias, se consideran solo aquellas que tengan un mínimo de estudiantes aprobados. Lo mismo para el análisis de estudiantes
MATERIAS_APROBADAS = 7
ESTUDIANTES_APROBADOS = 15
# Para ver los nombres completos de los nombres de materias
pd.set_option('display.max_colwidth', None)
Fairness#
Bajo este contexto, una materia injusta puede dar nota baja a “buenos estudiantes” y/o nota alta a “malos estudiantes”.
(
df_notas[(df_notas['cant_padrones'] >= ESTUDIANTES_APROBADOS)]
[['materia_id', 'materia_nombre', 'F']]
.drop_duplicates()
.sort_values('F', ascending=True)
.head(15)
)
materia_id | materia_nombre | F | |
---|---|---|---|
94046 | 76.27 | Control Estadístico de Procesos | 0.646846 |
90722 | 64.13 | Estabilidad III B | 0.651707 |
99560 | 91.25 | Estadística Aplicada III | 0.655873 |
90993 | 71.01 | Introducción a la Economía y Organización de la Empresa | 0.658874 |
81975 | 91.09 | Economía | 0.660895 |
79428 | 91.19 | Introducción a la Economía y Organización de la Empresa | 0.673984 |
101542 | 88.07 | Tránsito | 0.683709 |
100355 | 86.57 | Acústica | 0.691098 |
67373 | 91.31 | Investigación Operativa II | 0.694407 |
100426 | 62.07 | Mecánica II | 0.701697 |
14868 | 81.11 | Matemática Discreta | 0.702132 |
64591 | 91.07 | Investigación Operativa I | 0.708240 |
92740 | 76.48 | Evaluación de Propiedades Físicas | 0.710065 |
73354 | 63.14 | Química Orgánica | 0.716513 |
100987 | 70.02 | Geometría Descriptiva | 0.716515 |
Las materias justas suelen asignar notas conservadoras que tienden de 6 a 8.
(
df_notas[(df_notas['cant_padrones'] >= ESTUDIANTES_APROBADOS)]
[['materia_id', 'materia_nombre', 'F']]
.drop_duplicates()
.sort_values('F', ascending=False)
.head(15)
)
materia_id | materia_nombre | F | |
---|---|---|---|
82360 | 94.01 | Hormigón I | 0.896680 |
1083 | 85.02 | Electrotecnia | 0.887611 |
99153 | 75.69 | Sistemas Automáticos de Diagnóstico y Detección Fallas II | 0.881780 |
86007 | 88.13 | Puertos y Vías Navegables A | 0.880578 |
66885 | 92.06 | Automatización Industrial y Robótica | 0.877743 |
92104 | 94.13 | Patología de la Construcción | 0.877140 |
85080 | 89.11 | Ingeniería Sanitaria I | 0.875276 |
98868 | 67.30 | Combustión | 0.874913 |
91544 | 67.50 | Materiales Ferrosos y sus Aplicaciones | 0.872527 |
93246 | 76.52 | Operaciones Unitarias de Transferencia de Materia | 0.870980 |
24819 | 75.10 | Técnicas de Diseño | 0.865805 |
86481 | 91.16 | Legislación y Ejercicio Profesional de la Ingeniería Civil | 0.864699 |
50306 | 83.02 | Química Aplicada | 0.864635 |
1994 | 85.20 | Energías Renovables | 0.863711 |
100764 | 88.03 | Puertos y Vías Navegables B | 0.863584 |
Goodness#
Nota que una materia “confiable” le podría dar a tal estudiante. No es lo mismo que el promedio pero existe una correlación.
df_notas['G_norm'] = df_notas['G'] * 3 + 7
(
df_notas[(df_notas['cant_materias'] >= MATERIAS_APROBADAS)]
[['Padron', 'Carrera', 'G_norm', 'nota_promedio']]
.drop_duplicates()
.sort_values('G_norm', ascending=True)
.head(15)
)
Padron | Carrera | G_norm | nota_promedio | |
---|---|---|---|---|
41586 | 103444 | industrial | 5.380374 | 5.000000 |
42217 | 97478 | industrial | 5.410048 | 5.023256 |
71647 | 102950 | quimica | 5.520623 | 5.187500 |
41819 | 108112 | industrial | 5.575080 | 5.250000 |
5262 | 95536 | informatica | 5.592565 | 5.210526 |
41822 | 104040 | industrial | 5.644006 | 5.333333 |
30531 | 101309 | civil | 5.684993 | 5.384615 |
42535 | 99651 | industrial | 5.686176 | 5.388889 |
461 | pelepelepele | electricista | 5.695637 | 5.421053 |
462 | gules | electricista | 5.695637 | 5.421053 |
463 | morsin | electricista | 5.695637 | 5.421053 |
41885 | 104736 | industrial | 5.735251 | 5.428571 |
41817 | 106346 | industrial | 5.761698 | 5.428571 |
41739 | 103464 | industrial | 5.777393 | 5.444444 |
408 | 95568 | electricista | 5.779901 | 5.478261 |
(
df_notas[(df_notas['cant_materias'] >= MATERIAS_APROBADAS)]
[['Padron', 'Carrera', 'G_norm', 'nota_promedio']]
.drop_duplicates()
.sort_values('G_norm', ascending=False)
.head(15)
)
Padron | Carrera | G_norm | nota_promedio | |
---|---|---|---|---|
71729 | 100623 | quimica | 8.866571 | 9.280000 |
30407 | 105895 | civil | 8.728318 | 9.076923 |
30394 | 109522 | civil | 8.715727 | 9.125000 |
5335 | 105646 | informatica | 8.708965 | 9.083333 |
5666 | 105836 | informatica | 8.617090 | 8.950000 |
5380 | 108814 | informatica | 8.610925 | 9.000000 |
36144 | 110456 | electronica | 8.570352 | 8.931034 |
36326 | 103839 | electronica | 8.554069 | 8.903226 |
5263 | 105798 | informatica | 8.549445 | 8.888889 |
94580 | 0 | alimentos | 8.548754 | 8.941176 |
71526 | 0 | quimica | 8.548754 | 8.833333 |
42212 | 4383567 | industrial | 8.532443 | 8.850000 |
5627 | 102145 | informatica | 8.516730 | 8.823529 |
5264 | 102749 | informatica | 8.491980 | 8.827586 |
30388 | 105536 | civil | 8.476696 | 8.760000 |
Reliability#
Una nota poco confiable puede estar en dos extremos: una nota alta a un estudiante con un desempeño general bajo, o una nota baja a un estudiante con un buen desempeño
(
df_notas[
(df_notas['cant_padrones'] >= ESTUDIANTES_APROBADOS)
& (df_notas['cant_materias'] >= MATERIAS_APROBADAS)
][['Padron', 'Carrera', 'materia_id', 'materia_nombre', 'materia_nota', 'R', 'G_norm']]
.drop_duplicates()
.sort_values('R', ascending=True)
.head(15)
)
Padron | Carrera | materia_id | materia_nombre | materia_nota | R | G_norm | |
---|---|---|---|---|---|---|---|
82291 | 105370 | civil | 91.09 | Economía | 4.0 | 0.499946 | 7.966017 |
79501 | 103221 | electronica | 91.19 | Introducción a la Economía y Organización de la Empresa | 4.0 | 0.507515 | 7.953721 |
91054 | 102538 | mecanica | 71.01 | Introducción a la Economía y Organización de la Empresa | 4.0 | 0.507557 | 7.862557 |
81992 | 104497 | civil | 91.09 | Economía | 4.0 | 0.511299 | 7.829780 |
91023 | 103277 | mecanica | 71.01 | Introducción a la Economía y Organización de la Empresa | 4.0 | 0.517744 | 7.740315 |
82260 | 103365 | civil | 91.09 | Economía | 4.0 | 0.517816 | 7.751578 |
61500 | 100735 | industrial | 97.01 | Higiene y Seguridad Industrial | 10.0 | 0.518093 | 5.800418 |
91047 | 94286 | mecanica | 71.01 | Introducción a la Economía y Organización de la Empresa | 4.0 | 0.518956 | 7.725769 |
82155 | 107756 | civil | 91.09 | Economía | 4.0 | 0.520227 | 7.722652 |
91038 | 102539 | mecanica | 71.01 | Introducción a la Economía y Organización de la Empresa | 4.0 | 0.520892 | 7.702543 |
94085 | 87699 | quimica | 76.27 | Control Estadístico de Procesos | 10.0 | 0.521119 | 6.372350 |
20012 | 103371 | informatica | 61.07 | Matemática Discreta | 4.0 | 0.521172 | 8.108515 |
79494 | 102176 | electronica | 91.19 | Introducción a la Economía y Organización de la Empresa | 4.0 | 0.522984 | 7.768100 |
860 | 91218 | electricista | 81.03 | Probabilidad y Estadística A | 4.0 | 0.523975 | 8.093892 |
33764 | 103789 | civil | 81.03 | Probabilidad y Estadística A | 4.0 | 0.525280 | 8.078230 |
Una nota confiable es la más esperable para asignarle al estudiante según su valor de G.
(
df_notas[
(df_notas['cant_padrones'] >= ESTUDIANTES_APROBADOS)
& (df_notas['cant_materias'] >= MATERIAS_APROBADAS)
][['Padron', 'Carrera', 'materia_id', 'materia_nombre', 'materia_nota', 'R', 'G_norm']]
.drop_duplicates()
.sort_values('R', ascending=False)
.head(15)
)
Padron | Carrera | materia_id | materia_nombre | materia_nota | R | G_norm | |
---|---|---|---|---|---|---|---|
82604 | 100726 | civil | 94.01 | Hormigón I | 7.0 | 0.947925 | 6.995015 |
82412 | 100291 | civil | 94.01 | Hormigón I | 6.0 | 0.947688 | 6.007822 |
82618 | 97875 | civil | 94.01 | Hormigón I | 6.0 | 0.947657 | 5.991802 |
82464 | 104279 | civil | 94.01 | Hormigón I | 8.0 | 0.947465 | 7.989495 |
82387 | 106102 | civil | 94.01 | Hormigón I | 8.0 | 0.946584 | 8.021076 |
82578 | 102187 | civil | 94.01 | Hormigón I | 8.0 | 0.946132 | 7.973500 |
82607 | 100875 | civil | 94.01 | Hormigón I | 7.0 | 0.945954 | 7.028638 |
82615 | 94275 | civil | 94.01 | Hormigón I | 7.0 | 0.945846 | 7.029928 |
82645 | 103213 | civil | 94.01 | Hormigón I | 7.0 | 0.945705 | 6.968375 |
82606 | 105370 | civil | 94.01 | Hormigón I | 8.0 | 0.945508 | 7.966017 |
82641 | 100531 | civil | 94.01 | Hormigón I | 7.0 | 0.945057 | 6.960601 |
82438 | 103332 | civil | 94.01 | Hormigón I | 7.0 | 0.944874 | 7.041593 |
82655 | 101337 | civil | 94.01 | Hormigón I | 6.0 | 0.944285 | 6.048665 |
82624 | 102632 | civil | 94.01 | Hormigón I | 7.0 | 0.943380 | 6.940482 |
82381 | 102246 | civil | 94.01 | Hormigón I | 7.0 | 0.942920 | 6.934956 |
Reliability de un padrón particular#
Considerando un único padrón podemos ver sus notas “menos confiable” considerando el cuantil 0.1 de R
(
df_notas[
(df_notas['Padron'] == utils.PADRON)
& (df_notas['R'] <= df_notas['R'].quantile(0.10))
][['Padron', 'Carrera', 'materia_id', 'materia_nombre', 'materia_nota', 'R', 'G_norm']]
.drop_duplicates()
.sort_values('R', ascending=True)
.head(5)
)
Padron | Carrera | materia_id | materia_nombre | materia_nota | R | G_norm | |
---|---|---|---|---|---|---|---|
19902 | 100029 | informatica | 61.07 | Matemática Discreta | 4.0 | 0.597084 | 7.197567 |
20178 | 100029 | informatica | 75.31 | Teoría de Lenguaje | 10.0 | 0.626889 | 7.197567 |
5320 | 100029 | informatica | 62.01 | Física I A | 4.0 | 0.636554 | 7.197567 |
28005 | 100029 | informatica | 66.02 | Laboratorio | 4.0 | 0.654815 | 7.197567 |
18730 | 100029 | informatica | 75.08 | Sistemas Operativos | 10.0 | 0.658460 | 7.197567 |