Saltar al contenido principal

🏆 Cálculo de Leaderboard

Visión General

El leaderboard de PuntoHack utiliza un sistema de scoring ponderado que combina:

  1. Evaluación técnica (12 criterios estándar)
  2. Evaluación de challenges (opcional, 20% del score)

Fórmula de Cálculo

Score Técnico (80% del total)

// Por cada submission
const technicalScore = criteria.reduce((total, criterion) => {
// Obtener scores de todos los jueces vigentes para este criterio
const scoresForCriterion = scores.filter(
s => s.criterionId === criterion.id &&
activeJudgeIds.includes(s.judgeId)
);

// Promediar scores de jueces
const averageScore = scoresForCriterion.length > 0
? scoresForCriterion.reduce((sum, s) => sum + s.value, 0) / scoresForCriterion.length
: 0;

// Aplicar peso del criterio
return total + (averageScore * criterion.weight);
}, 0);

// Normalizar a 100 puntos máximo
const maxPossibleScore = criteria.reduce((sum, c) => sum + (c.maxScore * c.weight), 0);
const normalizedTechnicalScore = maxPossibleScore > 0
? (technicalScore / maxPossibleScore) * 100
: 0;

Score de Challenges (20% del total)

// Solo challenges aprobados por sponsors
const approvedEvaluations = submission.challengeEvaluations.filter(
e => e.sponsorApprovalStatus === 'APPROVED'
);

if (approvedEvaluations.length > 0) {
// Calcular score promedio ponderado por challenge
const challengeScores = approvedEvaluations.map(evaluation => {
return (
evaluation.fulfillmentScore * 0.4 + // 40% cumplimiento
evaluation.technicalScore * 0.3 + // 30% técnico
evaluation.adoptionScore * 0.2 + // 20% adopción
evaluation.documentationScore * 0.1 // 10% documentación
);
});

const averageChallengeScore = challengeScores.reduce((sum, val) => sum + val, 0) / challengeScores.length;

// Agregar 20% al score total
challengeScore = averageChallengeScore * 0.2;
}

Score Final

const finalScore = Math.min(
normalizedTechnicalScore + challengeScore,
100 // Máximo 100 puntos
);

Diagrama de Cálculo

Ejemplo Numérico Completo

Configuración del Hackathon

Criterios:

  • Stack Tecnológico (weight: 2, maxScore: 10)
  • Arquitectura (weight: 3, maxScore: 10)
  • Características (weight: 2, maxScore: 10)
  • Desafíos Técnicos (weight: 2, maxScore: 10)
  • Mejoras Futuras (weight: 1, maxScore: 10)
  • Documentación Técnica (weight: 2, maxScore: 10)
  • Documentación API (weight: 1, maxScore: 10)
  • Guía Despliegue (weight: 1, maxScore: 10)
  • Cobertura Tests (weight: 2, maxScore: 10)
  • Métricas Rendimiento (weight: 1, maxScore: 10)
  • Repositorio (weight: 2, maxScore: 10)
  • Demo Funcional (weight: 1, maxScore: 10)

Total weight: 20
Max possible score raw: 20 × 10 = 200

Scores de Jueces

Submission A - 3 jueces evaluaron:

CriterioJuez 1Juez 2Juez 3PromedioWeightScore Ponderado
Stack9898.67217.34
Arquitectura8988.33324.99
Características7867.00214.00
Desafíos8787.67215.34
Mejoras6766.3316.33
Doc Técnica9898.67217.34
Doc API8787.6717.67
Guía Despliegue7877.3317.33
Tests8988.33216.66
Rendimiento7877.3317.33
Repositorio9898.67217.34
Demo8988.3318.33

Total técnico raw: 154.32
Normalizado: (154.32 / 200) × 100 = 77.16 puntos

Challenge Evaluation (Opcional)

Si la submission tiene challenge aprobado:

Evaluación del Challenge:

  • Fulfillment: 85/100
  • Technical: 80/100
  • Adoption: 75/100
  • Documentation: 90/100

Cálculo:

Challenge Score = (85 × 0.4) + (80 × 0.3) + (75 × 0.2) + (90 × 0.1)
= 34 + 24 + 15 + 9
= 82 puntos

Agregar al total: 82 × 0.2 = 16.4 puntos adicionales

Score Final

Score Final = 77.16 (técnico) + 16.4 (challenges)
= 93.56 puntos

Implementación Completa

export async function calculateLeaderboard(hackathonId: string): Promise<LeaderboardEntry[]> {
// 1. Obtener todas las submissions
const submissions = await db.submission.findMany({
where: { hackathonId },
include: {
team: {
include: {
members: {
include: { profile: true },
},
},
},
scores: {
include: {
judge: { select: { id: true } },
criterion: {
select: {
id: true,
weight: true,
maxScore: true,
},
},
},
},
challengeEvaluations: {
where: {
sponsorApprovalStatus: 'APPROVED',
},
select: {
fulfillmentScore: true,
technicalScore: true,
adoptionScore: true,
documentationScore: true,
},
},
},
});

// 2. Obtener jueces vigentes
const activeJudges = await db.hackathonJudge.findMany({
where: { hackathonId },
select: { profileId: true },
});
const activeJudgeIds = new Set(activeJudges.map(j => j.profileId));

// 3. Obtener criterios estándar (12)
const allCriteria = await db.criterion.findMany({
where: { hackathonId },
});

const requiredCriteriaNames = [
'Stack Tecnológico',
'Arquitectura del Sistema',
'Características Principales',
'Resolución de Desafíos Técnicos',
'Visión y Mejoras Futuras',
'Documentación Técnica',
'Documentación de API',
'Guía de Despliegue',
'Cobertura de Tests',
'Métricas de Rendimiento',
'Repositorio y Código',
'Demo Funcional',
];

const criteria = allCriteria.filter(c => requiredCriteriaNames.includes(c.name));

// 4. Calcular maxPossibleScoreRaw
const maxPossibleScoreRaw = criteria.reduce(
(sum, c) => sum + (c.maxScore * c.weight),
0
);

// 5. Calcular score para cada submission
const leaderboard: LeaderboardEntry[] = submissions.map(submission => {
// Filtrar scores solo de jueces vigentes
const validScores = submission.scores.filter(
score => activeJudgeIds.has(score.judge.id)
);

// Agrupar scores por criterio
const scoresByCriterion = new Map<string, number[]>();
validScores.forEach(score => {
const criterionId = score.criterionId;
if (!scoresByCriterion.has(criterionId)) {
scoresByCriterion.set(criterionId, []);
}
scoresByCriterion.get(criterionId)!.push(score.value);
});

// Calcular score técnico
let technicalScore = 0;
criteria.forEach(criterion => {
const scoresForCriterion = scoresByCriterion.get(criterion.id) || [];
if (scoresForCriterion.length > 0) {
const averageScore = scoresForCriterion.reduce((sum, val) => sum + val, 0) / scoresForCriterion.length;
technicalScore += averageScore * criterion.weight;
}
});

// Normalizar score técnico
const normalizedTechnicalScore = maxPossibleScoreRaw > 0
? (technicalScore / maxPossibleScoreRaw) * 100
: 0;

// Calcular score de challenges
let challengeScore = 0;
if (submission.challengeEvaluations.length > 0) {
const challengeScores = submission.challengeEvaluations.map(evaluation => {
return (
evaluation.fulfillmentScore * 0.4 +
evaluation.technicalScore * 0.3 +
evaluation.adoptionScore * 0.2 +
evaluation.documentationScore * 0.1
);
});

const averageChallengeScore = challengeScores.reduce((sum, val) => sum + val, 0) / challengeScores.length;
challengeScore = averageChallengeScore * 0.2;
}

// Score final
const finalScore = Math.min(normalizedTechnicalScore + challengeScore, 100);

return {
position: 0, // Se asignará después
team: {
id: submission.team.id,
name: submission.team.name,
code: submission.team.code,
members: submission.team.members.map(m => ({ profile: m.profile })),
},
submission: {
id: submission.id,
title: submission.title,
description: submission.description,
repoUrl: submission.repoUrl,
demoUrl: submission.demoUrl,
},
weightedScore: Math.round(finalScore * 100) / 100,
totalScore: Math.round(normalizedTechnicalScore * 100) / 100,
maxPossibleScore: 100,
challengeScore: Math.round(challengeScore * 100) / 100,
};
});

// 6. Ordenar por score descendente
leaderboard.sort((a, b) => b.weightedScore - a.weightedScore);

// 7. Asignar posiciones
leaderboard.forEach((entry, index) => {
entry.position = index + 1;
});

return leaderboard;
}

Consideraciones Importantes

Solo Jueces Vigentes

Regla: Solo se consideran scores de jueces con HackathonJudge vigente.

Razón: Si un juez es removido, sus scores permanecen en la DB pero no cuentan para el cálculo.

// Obtener jueces vigentes
const activeJudges = await db.hackathonJudge.findMany({
where: { hackathonId },
});

// Filtrar scores
const validScores = submission.scores.filter(
score => activeJudgeIds.has(score.judge.id)
);

Criterios Estándar

Regla: Solo se usan los 12 criterios estándar para el cálculo.

Razón: Asegurar consistencia en la evaluación técnica.

Empates

Regla: Se permiten empates. Múltiples equipos pueden tener el mismo score.

Implementación: No hay criterio de desempate en el MVP.

Challenges Opcionales

Regla: Los challenges son opcionales. Si una submission no tiene challenges aprobados, su score es solo técnico.

Implementación: challengeScore es 0 si no hay evaluaciones aprobadas.

Próximos Pasos


Siguiente: Evaluation System