45. Proyecto integrador de teoría de conjuntos aplicada

El proyecto integra pertenencia, operaciones, relaciones, cardinalidad, producto cartesiano y funciones para construir un recomendador simple con JavaScript.

45.1 Objetivo del proyecto

El objetivo es construir un recomendador simple de cursos a partir de intereses del usuario, temas disponibles y relaciones entre cursos y etiquetas.

El proyecto usa teoría de conjuntos para filtrar, comparar, combinar y ordenar información.

45.2 Problema a resolver

Una plataforma tiene cursos con distintas etiquetas. Un usuario indica sus intereses y el sistema debe recomendar cursos relacionados.

Intereses del usuario = {programación, datos, lógica}
Curso recomendado = curso cuyas etiquetas coinciden mejor con esos intereses

La recomendación se basará en intersección, unión, diferencia y cardinalidad.

45.3 Conceptos integrados

El proyecto reúne varios conceptos vistos durante el curso.

  • Conjuntos y pertenencia.
  • Subconjuntos y cardinalidad.
  • Unión, intersección y diferencia.
  • Relaciones entre cursos y etiquetas.
  • Funciones para transformar datos.
  • Similitud entre conjuntos.

45.4 Modelo de datos

Cada curso tendrá un identificador, un título y un conjunto de etiquetas.

const cursos = [
  {
    id: 1,
    titulo: "Lógica para programación",
    etiquetas: new Set(["lógica", "programación", "matemática"])
  },
  {
    id: 2,
    titulo: "Bases de datos",
    etiquetas: new Set(["datos", "SQL", "programación"])
  },
  {
    id: 3,
    titulo: "Introducción a grafos",
    etiquetas: new Set(["grafos", "relaciones", "matemática"])
  }
];

console.log(cursos);

45.5 Conjunto de intereses

Los intereses del usuario se representarán como un conjunto.

const interesesUsuario = new Set(["programación", "datos", "lógica"]);

console.log(interesesUsuario);

Usar Set evita intereses repetidos y permite consultas de pertenencia.

45.6 Funciones básicas de conjuntos

Primero definimos operaciones reutilizables.

function union(a, b) {
  return new Set([...a, ...b]);
}

function interseccion(a, b) {
  return new Set([...a].filter(x => b.has(x)));
}

function diferencia(a, b) {
  return new Set([...a].filter(x => !b.has(x)));
}

const A = new Set([1, 2, 3]);
const B = new Set([3, 4, 5]);

console.log(union(A, B));
console.log(interseccion(A, B));
console.log(diferencia(A, B));

Estas funciones serán la base del recomendador.

45.7 Coincidencias entre usuario y curso

Las coincidencias se obtienen con la intersección entre intereses y etiquetas del curso.

const cursos = [
  {
    id: 1,
    titulo: "Lógica para programación",
    etiquetas: new Set(["lógica", "programación", "matemática"])
  }
];

const interesesUsuario = new Set(["programación", "datos", "lógica"]);

function interseccion(a, b) {
  return new Set([...a].filter(x => b.has(x)));
}

function coincidencias(usuario, curso) {
  return interseccion(usuario, curso.etiquetas);
}

console.log(coincidencias(interesesUsuario, cursos[0]));

Cuantos más elementos tenga la intersección, mayor será la afinidad básica.

45.8 Puntaje por cardinalidad

Un primer puntaje puede ser la cardinalidad del conjunto de coincidencias.

const cursos = [
  {
    id: 1,
    titulo: "Lógica para programación",
    etiquetas: new Set(["lógica", "programación", "matemática"])
  }
];

const interesesUsuario = new Set(["programación", "datos", "lógica"]);

function interseccion(a, b) {
  return new Set([...a].filter(x => b.has(x)));
}

function coincidencias(usuario, curso) {
  return interseccion(usuario, curso.etiquetas);
}

function puntajeSimple(usuario, curso) {
  return coincidencias(usuario, curso).size;
}

console.log(puntajeSimple(interesesUsuario, cursos[0])); // 2

El curso de lógica coincide con programación y lógica.

45.9 Similitud de Jaccard

Para comparar mejor conjuntos de distinto tamaño, se puede usar la similitud de Jaccard.

J(A, B) = |A ∩ B| / |A ∪ B|
function union(a, b) {
  return new Set([...a, ...b]);
}

function interseccion(a, b) {
  return new Set([...a].filter(x => b.has(x)));
}

function jaccard(a, b) {
  const comun = interseccion(a, b);
  const total = union(a, b);

  return comun.size / total.size;
}

const usuario = new Set(["programación", "datos", "lógica"]);
const curso = new Set(["datos", "SQL", "programación"]);

console.log(jaccard(usuario, curso));

45.10 Calcular afinidad

La afinidad entre usuario y curso se puede calcular aplicando Jaccard entre intereses y etiquetas.

const cursos = [
  {
    id: 2,
    titulo: "Bases de datos",
    etiquetas: new Set(["datos", "SQL", "programación"])
  }
];

const interesesUsuario = new Set(["programación", "datos", "lógica"]);

function union(a, b) {
  return new Set([...a, ...b]);
}

function interseccion(a, b) {
  return new Set([...a].filter(x => b.has(x)));
}

function jaccard(a, b) {
  const comun = interseccion(a, b);
  const total = union(a, b);

  return comun.size / total.size;
}

function afinidad(usuario, curso) {
  return jaccard(usuario, curso.etiquetas);
}

console.log(afinidad(interesesUsuario, cursos[0]));

El resultado es un número entre 0 y 1.

45.11 Generar recomendaciones

Ahora se calcula un puntaje para cada curso y se ordena de mayor a menor.

const cursos = [
  {
    id: 1,
    titulo: "Lógica para programación",
    etiquetas: new Set(["lógica", "programación", "matemática"])
  },
  {
    id: 2,
    titulo: "Bases de datos",
    etiquetas: new Set(["datos", "SQL", "programación"])
  },
  {
    id: 3,
    titulo: "Introducción a grafos",
    etiquetas: new Set(["grafos", "relaciones", "matemática"])
  }
];

const interesesUsuario = new Set(["programación", "datos", "lógica"]);

function union(a, b) {
  return new Set([...a, ...b]);
}

function interseccion(a, b) {
  return new Set([...a].filter(x => b.has(x)));
}

function coincidencias(usuario, curso) {
  return interseccion(usuario, curso.etiquetas);
}

function jaccard(a, b) {
  const comun = interseccion(a, b);
  const total = union(a, b);

  return comun.size / total.size;
}

function afinidad(usuario, curso) {
  return jaccard(usuario, curso.etiquetas);
}

function recomendar(usuario, cursos) {
  return cursos
    .map(curso => ({
      ...curso,
      puntaje: afinidad(usuario, curso),
      coincidencias: [...coincidencias(usuario, curso)]
    }))
    .filter(curso => curso.puntaje > 0)
    .sort((a, b) => b.puntaje - a.puntaje);
}

console.log(recomendar(interesesUsuario, cursos));

La lista final contiene solo cursos con alguna coincidencia.

45.12 Detectar intereses no cubiertos

La diferencia permite saber qué intereses del usuario no aparecen en las etiquetas de un curso.

const cursos = [
  {
    id: 3,
    titulo: "Introducción a grafos",
    etiquetas: new Set(["grafos", "relaciones", "matemática"])
  }
];

const interesesUsuario = new Set(["programación", "datos", "lógica"]);

function diferencia(a, b) {
  return new Set([...a].filter(x => !b.has(x)));
}

function interesesNoCubiertos(usuario, curso) {
  return diferencia(usuario, curso.etiquetas);
}

console.log(interesesNoCubiertos(interesesUsuario, cursos[0]));

Esto ayuda a explicar por qué un curso recibió menor puntaje.

45.13 Relación curso-etiqueta

También se puede representar la información como una relación entre cursos y etiquetas.

R ⊆ Cursos × Etiquetas
const cursos = [
  {
    id: 1,
    titulo: "Lógica para programación",
    etiquetas: new Set(["lógica", "programación", "matemática"])
  },
  {
    id: 2,
    titulo: "Bases de datos",
    etiquetas: new Set(["datos", "SQL", "programación"])
  }
];

const relacionCursoEtiqueta = cursos.flatMap(curso =>
  [...curso.etiquetas].map(etiqueta => [curso.id, etiqueta])
);

console.log(relacionCursoEtiqueta);

Cada par ordenado indica que un curso tiene una etiqueta.

45.14 Producto cartesiano para análisis

El producto cartesiano entre usuarios y cursos permite evaluar todas las combinaciones posibles.

const cursos = [
  { id: 1, titulo: "Lógica para programación" },
  { id: 2, titulo: "Bases de datos" },
  { id: 3, titulo: "Introducción a grafos" }
];

function productoCartesiano(a, b) {
  const resultado = [];

  for (const x of a) {
    for (const y of b) {
      resultado.push([x, y]);
    }
  }

  return resultado;
}

const usuarios = ["usuario1", "usuario2"];
console.log(productoCartesiano(usuarios, cursos.map(c => c.id)));

45.15 Explicar una recomendación

Un sistema más útil no solo recomienda, también explica el motivo.

const cursos = [
  {
    id: 1,
    titulo: "Lógica para programación",
    etiquetas: new Set(["lógica", "programación", "matemática"])
  }
];

const interesesUsuario = new Set(["programación", "datos", "lógica"]);

function union(a, b) {
  return new Set([...a, ...b]);
}

function interseccion(a, b) {
  return new Set([...a].filter(x => b.has(x)));
}

function diferencia(a, b) {
  return new Set([...a].filter(x => !b.has(x)));
}

function coincidencias(usuario, curso) {
  return interseccion(usuario, curso.etiquetas);
}

function interesesNoCubiertos(usuario, curso) {
  return diferencia(usuario, curso.etiquetas);
}

function jaccard(a, b) {
  const comun = interseccion(a, b);
  const total = union(a, b);

  return comun.size / total.size;
}

function afinidad(usuario, curso) {
  return jaccard(usuario, curso.etiquetas);
}

function explicar(usuario, curso) {
  const comunes = coincidencias(usuario, curso);
  const faltantes = interesesNoCubiertos(usuario, curso);

  return {
    curso: curso.titulo,
    coincideEn: [...comunes],
    noCubre: [...faltantes],
    puntaje: afinidad(usuario, curso)
  };
}

console.log(explicar(interesesUsuario, cursos[0]));

La explicación se construye con intersección y diferencia.

45.16 Filtrar por requisitos mínimos

Un curso puede requerir que ciertos intereses estén presentes.

const cursos = [
  {
    id: 1,
    titulo: "Lógica para programación",
    etiquetas: new Set(["lógica", "programación", "matemática"])
  },
  {
    id: 2,
    titulo: "Bases de datos",
    etiquetas: new Set(["datos", "SQL", "programación"])
  },
  {
    id: 3,
    titulo: "Introducción a grafos",
    etiquetas: new Set(["grafos", "relaciones", "matemática"])
  }
];

function esSubconjunto(a, b) {
  return [...a].every(x => b.has(x));
}

const requisitos = new Set(["programación"]);

const compatibles = cursos.filter(curso =>
  esSubconjunto(requisitos, curso.etiquetas)
);

console.log(compatibles);

La condición de compatibilidad se expresa como inclusión de conjuntos.

45.17 Validar datos del proyecto

Antes de recomendar, conviene validar que cada curso tenga identificador, título y etiquetas.

const cursos = [
  {
    id: 1,
    titulo: "Lógica para programación",
    etiquetas: new Set(["lógica", "programación", "matemática"])
  },
  {
    id: 2,
    titulo: "Bases de datos",
    etiquetas: new Set(["datos", "SQL", "programación"])
  }
];

function cursoValido(curso) {
  return Number.isInteger(curso.id) &&
    typeof curso.titulo === "string" &&
    curso.etiquetas instanceof Set;
}

console.log(cursos.every(cursoValido)); // true

Validar la estructura evita errores al operar con conjuntos.

45.18 Posibles mejoras

El proyecto puede extenderse de varias maneras.

  • Agregar pesos distintos para cada etiqueta.
  • Separar intereses obligatorios y opcionales.
  • Guardar cursos visitados y excluirlos con diferencia.
  • Mostrar diagramas de Venn para explicar coincidencias.
  • Usar relaciones entre cursos para recomendar rutas de aprendizaje.

45.19 Código integrado

El flujo principal del proyecto queda organizado en pocas funciones.

const cursos = [
  {
    id: 1,
    titulo: "Lógica para programación",
    etiquetas: new Set(["lógica", "programación", "matemática"])
  },
  {
    id: 2,
    titulo: "Bases de datos",
    etiquetas: new Set(["datos", "SQL", "programación"])
  },
  {
    id: 3,
    titulo: "Introducción a grafos",
    etiquetas: new Set(["grafos", "relaciones", "matemática"])
  }
];

const interesesUsuario = new Set(["programación", "datos", "lógica"]);

function union(a, b) {
  return new Set([...a, ...b]);
}

function interseccion(a, b) {
  return new Set([...a].filter(x => b.has(x)));
}

function coincidencias(usuario, curso) {
  return interseccion(usuario, curso.etiquetas);
}

function jaccard(a, b) {
  const comun = interseccion(a, b);
  const total = union(a, b);

  return comun.size / total.size;
}

function afinidad(usuario, curso) {
  return jaccard(usuario, curso.etiquetas);
}

function recomendar(usuario, cursos) {
  return cursos
    .map(curso => ({
      ...curso,
      puntaje: afinidad(usuario, curso),
      coincidencias: [...coincidencias(usuario, curso)]
    }))
    .filter(curso => curso.puntaje > 0)
    .sort((a, b) => b.puntaje - a.puntaje);
}

const recomendaciones = recomendar(interesesUsuario, cursos);

for (const curso of recomendaciones) {
  console.log({
    titulo: curso.titulo,
    puntaje: curso.puntaje,
    coincidencias: curso.coincidencias
  });
}

El resultado final puede mostrarse en consola, en una página web o en una interfaz interactiva.

45.20 Conclusión

Este proyecto muestra cómo la teoría de conjuntos puede convertirse en una solución concreta. Las operaciones matemáticas permiten filtrar datos, comparar intereses, medir similitud, explicar recomendaciones y modelar relaciones.

La teoría de conjuntos no es solo una base abstracta: también es una herramienta práctica para programar sistemas claros, razonables y fáciles de extender.