Guía de diseño de preguntas con qbank
Esta guía explica cómo diseñar buenas preguntas con qbank: qué hace el
motor con lo que escribes, qué patrón de pregunta conviene adoptar y qué
errores de redacción se repiten una y otra vez. No es la referencia de la
API (eso es el Manual, Manual.org o Manual.pdf) ni un recorrido por
ejemplos (el cuaderno Tutorial.ipynb): es
lo que hay que tener en la cabeza antes de escribir una pregunta y lo que
hay que comprobar después.
Supone que tienes un proyecto creado con qbank-init (el README.org que
qbank-init deja en el proyecto explica el flujo de trabajo) y que sabes lanzar make lint y make amc
desde la carpeta del proyecto. Los ejemplos se escriben como
listas de Python, que se leen mejor; el JSON equivalente aparece una vez
completo en la sección 9 («El mismo ejemplo en JSON»). Todo lo que se dice vale
igual para los dos formatos.
1. La mecánica en una página
1.1. Las tres piezas
Una pregunta (ProblemaTipo) es una lista plana de componentes de tres tipos:
- Texto (una cadena): fragmento fijo del enunciado.
- Supuesto: fragmento de enunciado que, además de texto, aporta una
hipótesis lógica (
semantica) al resto de la variante. - Cuestion: una afirmación que se ofrece como opción de respuesta. Se
marca correcta si su
semanticaes consecuencia lógica de las hipótesis acumuladas en esa variante; si no, incorrecta.
Las dos últimas tienen un campo precond: la condición que deben cumplir
las hipótesis ya aceptadas (o los números del setup) para que ese
componente pueda aparecer. Si no se cumple, toda la variante se
descarta: no se muestra al alumno. precond no convierte una opción en
incorrecta; la quita de en medio.
Las fórmulas se escriben con calcprop: v('A') es una proposición
atómica cuyo nombre eliges tú y solo tiene que ser consistente dentro de la
misma pregunta; -, &, |, >> y ** son negación, conjunción,
disyunción, implicación y equivalencia; unoDe(A, B, C) («exactamente
una») y alguno(A, B, C) («al menos una») evitan anidar operadores. True
y False valen como fórmulas para afirmaciones siempre ciertas o siempre
falsas sea cual sea el mundo.
La pregunta que el motor se hace para cada Cuestion es «¿se deduce
esto de las hipótesis?», no «¿es verdad en el mundo real?». Esa
distinción está detrás de casi todo lo que sigue.
1.2. Vocabulario
- Hipótesis
- lo que la variante da por supuesto: las fórmulas de los
Supuestoactivos. Consecuencia lógica de las hipótesis es lo que se sigue necesariamente de ellas (si las hipótesis son ciertas, eso es cierto). Es lo único que el motor comprueba. - Etiqueta
- cada
v('nombre'): un nombre que tú eliges para una afirmación («el país es proteccionista», «la función crece»). Solo tiene que ser consistente dentro de la misma pregunta. - Axioma
- un
Supuestosin texto que añade una hipótesis en todas las variantes; casi siempre, que las etiquetas de los mundos son excluyentes (unoDe). - Variante
- cada una de las versiones concretas de la pregunta que ve un alumno: un enunciado y sus opciones, con las marcas de correcta o incorrecta ya calculadas.
- Mundo
- el conjunto de hipótesis activas en una variante; viene
determinado por qué
Supuestose han sorteado. Dos variantes con los mismosSupuestoviven en el mismo mundo aunque muestren opciones distintas. - Hueco
- cada posición de la pregunta que se convierte en una opción
para el alumno: una
Cuestionsuelta, o una lista deCuestionde la que se sortea una. - Parte
- cada bloque de opciones de una pregunta con varios (ver más abajo). La mayoría de preguntas tienen una sola.
- Opción por defecto
- la opción «Ninguna de las anteriores» que añade
export.last_choice; es correcta solo si las demás son incorrectas. - Vista profe
- el PDF de
make amc: una entrada por mundo (no por variante) con todas las cuestiones de cada hueco (no solo la sorteada), su marca y las rechazadas por su precondición. Consetup, una entrada por caso: cada combinación distinta de mundo y patrón de marcas que aparece al explorar varios sorteos.nueva_pregunta.pyimprime en texto las primeras entradas de esa vista (las rechazadas con·, sin su condición, y sin las explicaciones). - Instancia
- con
setupaleatorio, cada ejecución delsetup; conexport.instances = Ncada variante estructural se genera \(N\) veces con números distintos.
1.3. Listas = sorteo; elementos consecutivos = opciones
Cada componente de la lista plana es, o bien un valor suelto, o bien una lista de dos o más valores del mismo tipo. En cada variante:
- un valor suelto siempre se incluye;
- de una lista de \(N\) alternativas se elige una sola.
El número total de variantes es el producto de las longitudes de todas las listas. Y aquí está la consecuencia que rompe la intuición de casi todo el que empieza:
Una lista de varias
Cuestionno significa «estas \(N\) afirmaciones se muestran juntas como las \(N\) opciones de la pregunta». Significa «en cada variante se sorteará una de estas \(N\)». El alumno ve tantas opciones como elementos-Cuestion consecutivos haya en la lista plana (sueltos o cada uno como su propia lista de alternativas), más la opción por defecto siexport.last_choiceestá activo.
En el esquema siguiente, S1, S2, S3 son objetos Supuesto y C1a,
C1b, C2, … son objetos Cuestion; las cadenas entre comillas son texto
fijo:
[ "Enunciado ", ← cadena: texto fijo, aparece siempre [ S1, S2, S3 ], ← lista de Supuesto: 3 mundos, se sortea uno " Entonces:", ← cadena [ C1a, C1b ], ← hueco 1, lista de Cuestion: se muestra una de las dos C2, ← hueco 2, Cuestion suelta: se muestra siempre [ C3a, C3b, C3c ] ] ← hueco 3, lista de Cuestion: se muestra una de las tres Variantes: 3 × 2 × 1 × 3 = 18. Opciones por variante: 3 (+1 con last_choice).
Escribir las opciones como una única lista de cuatro Cuestion produce
preguntas de una sola opción; escribirlas todas sueltas muestra siempre
las mismas cuatro, sin variedad. lint avisa de las dos cosas: de la
primera con pocas-opciones, de la segunda con variantes («solo 1
variantes»).
Una lista de varias cadenas de texto también es válida (alternativas de redacción sin lógica asociada), pero se usa poco.
1.4. Mundos, axiomas y la regla de la negación
Una lista de varios Supuesto consecutivos son mundos alternativos: en
cada variante se activa uno, su texto entra en el enunciado y su fórmula
en las hipótesis. Es la forma natural de las preguntas «si se cumple A,
entonces…; si se cumple B, entonces…»: cada Cuestion lleva como
semantica la etiqueta del mundo (o de los mundos) en que es cierta, y
será correcta solo cuando ese sea el mundo sorteado.
Un Supuesto suelto con enunciado vacío es un axioma: añade su
fórmula sin añadir texto. Sirve para declarar relaciones entre etiquetas
que el motor no conoce. La más habitual: que los mundos son excluyentes,
unoDe(v('A'), v('B'), v('C')).
Sin ese axioma, el motor no puede deducir negaciones: de la hipótesis
v('crec') no se sigue -v('decr'), porque nada dice que ambas no puedan
ser ciertas a la vez. Una Cuestion con semantica=-v('decr') sería falsa
en todos los mundos (lint lo marca como ERROR negacion). Dos salidas:
formular en positivo (semantica=v('crec')) o añadir el axioma unoDe.
Para una afirmación cierta en varios mundos no hace falta axioma: basta
la disyunción de sus etiquetas (semantica=v('A') | v('B')).
1.5. Partes
Mientras encadenas elementos-Cuestion sin que aparezca ningún texto ni
Supuesto en medio, todos pertenecen a la misma parte (un bloque de
opciones). En cuanto, después de una Cuestion, vuelve a aparecer texto o
un Supuesto, se abre una parte nueva: un ejercicio con varios bloques
de opciones bajo un enunciado común. En Moodle se exporta como una
pregunta cloze (un bloque de opciones por parte) y en AMC como una única
pregunta con varios bloques de casillas, cada uno con su opción por
defecto. Las hipótesis se acumulan a lo largo de todas las partes. Para la
mayoría de preguntas basta una parte; no fuerces varias si no aportan
claridad.
1.6. Cómo se pegan los fragmentos del enunciado
Dos fragmentos consecutivos se concatenan tal cual si alguno de los dos ya
aporta separación (empieza o termina en espacio, o está vacío); si ninguno
la aporta, qbank inserta un espacio. Para controlar la puntuación: pega
la coma o el punto al fragmento anterior ("…,") y antepón tú el espacio
al siguiente (" entonces"). Y no pongas la puntuación justo después de
un Supuesto que pueda estar vacío: con [Supuesto("", …), Supuesto("y
$B$", …)] seguido de ", entonces" la variante vacía produce «\(A\) ,
entonces». Pon la coma dentro de los Supuesto no vacíos («y \(B\),») o
mueve el Supuesto vacío a otro sitio, y revisa todas las variantes
renderizadas.
1.7. Un ejemplo completo comentado
from qbank import *
p = ProblemaTipo(
[
"Sea $f$ una función ",
[ # mundos: en cada variante se sortea uno
Supuesto("estrictamente creciente", v('crec')),
Supuesto("estrictamente decreciente", v('decr')),
],
Supuesto("", unoDe(v('crec'), v('decr'))), # axioma: exactamente uno
" y sean $a < b$ dos puntos de su dominio. Entonces:",
[ # hueco 1: se muestra una de las dos
Cuestion("$f(a) < f(b)$", v('crec'),
exp="Si $f$ crece, $a<b$ implica $f(a)<f(b)$; si decrece, $f(a)>f(b)$."),
Cuestion("$f(a) > f(b)$", v('decr'),
exp="Si $f$ crece, $a<b$ implica $f(a)<f(b)$; si decrece, $f(a)>f(b)$."),
],
Cuestion("$f(a) = f(b)$", False, # hueco 2: se muestra siempre
exp="Una función estrictamente monótona no repite valores."),
[ # hueco 3
Cuestion("$f(b) - f(a) > 0$", v('crec'),
exp="El signo de $f(b)-f(a)$ con $a<b$ es el de la monotonía."),
Cuestion("$f(b) - f(a) < 0$", v('decr'),
exp="El signo de $f(b)-f(a)$ con $a<b$ es el de la monotonía."),
],
],
export={"last_choice": True, "cols": 2},
nombre="L-01-Monotonia",
)
Hay \(2 \times 2 \times 1 \times 2 = 8\) variantes; en todas el alumno ve tres opciones más «Ninguna de las anteriores». Dos de ellas, la 1 y la 6 (el listado no incluye la opción por defecto, que va siempre al final):
1 Sea $f$ una función estrictamente creciente y sean $a < b$ dos puntos de su dominio. Entonces:
✓ $f(a) < f(b)$
✗ $f(a) = f(b)$
✓ $f(b) - f(a) > 0$
6 Sea $f$ una función estrictamente decreciente y sean $a < b$ dos puntos de su dominio. Entonces:
✗ $f(a) < f(b)$
✗ $f(a) = f(b)$
✓ $f(b) - f(a) < 0$
Fíjate en que $f(a) > f(b)$ no está escrita como negación de la otra:
lleva v('decr') y es correcta solo en ese mundo. Y en que la opción fija
$f(a) = f(b)$ es siempre falsa: con last_choice eso no es un problema
(hay variantes en las que las tres reales son falsas y la opción por
defecto pasa a ser la correcta), pero es un distractor que el alumno
aprende a descartar; lint lo señala como INFO constante.
La tabla de mundos resume la lógica de un vistazo y es la forma más
rápida de auditarla. Se pide con make tabla PREGUNTA=<nombre> (con o
sin .json), que ejecuta python -m qbank.lint preguntas/<nombre>.json
--tabla: para esta pregunta, una vez guardada en preguntas/,
make tabla PREGUNTA=L-01-Monotonia; la plantilla de qbank-init trae
una versión con setup de la misma idea:
make tabla PREGUNTA=L-01-FuncionMonotona. Cada fila es
una Cuestion, identificada por su posición en la lista plana ([4.1]:
componente número 4 y, dentro de él, alternativa número 1, numerando ambos
desde 0), cada columna un mundo, y V / F si en ese mundo la cuestión es
correcta o incorrecta (- si su precondición la excluye):
Mundos:
M1: estrictamente creciente
M2: estrictamente decreciente
M1 M2
[4.0] $f(a) < f(b)$ V F
[4.1] $f(a) > f(b)$ F V
[5.0] $f(a) = f(b)$ F F
[6.0] $f(b) - f(a) > 0$ V F
[6.1] $f(b) - f(a) < 0$ F V
(La tabla se calcula con un solo sorteo del setup; si los mundos se
filtran con precondiciones sobre los números —ver más abajo— solo aparecen
los que ese sorteo admite. Por la misma razón, una Cuestion con
semantica lambda que sale constante en la tabla es solo INFO
constante: en otros sorteos puede cambiar. Para esas preguntas, la
vista profe de make amc es la referencia.)
Y lint dice exactamente lo que falta (comp 5[0] es la misma
numeración de la tabla; «semantica literal» significa que la fórmula es
el False literal, un distractor deliberado):
AVISO [variantes] solo 8 variantes (mínimo recomendado 16) INFO [constante] falsa en los 2 mundos en que aparece (semantica literal) @ comp 5[0]: $f(a) = f(b)$
Ocho variantes son pocas; la sección siguiente dice cómo conseguir más sin alargar la pregunta.
2. El patrón recomendado: tres huecos y la opción por defecto
2.1. Cuatro opciones con mezcla garantizada
Cada pregunta (cada parte, si hay varias) debe mostrar siempre el mismo número de opciones, con la garantía de que al menos una es correcta y al menos una incorrecta en toda variante (Moodle rechaza una pregunta sin ninguna correcta; un examen en papel con «todas verdaderas» regala la respuesta). Dos formas de lograrlo:
- Tres elementos-Cuestion consecutivos +
"export": {"last_choice": true}. La cuarta opción («Ninguna de las anteriores» en AMC, «Las demás opciones son falsas» en Moodle) la calcula el motor: es correcta si y solo si las tres reales son incorrectas. Eso garantiza la mezcla sea cual sea la combinación sorteada, sin pensar. Es el patrón por defecto; las preguntas de ejemplo deqbank-initlo siguen. - Cuatro elementos-Cuestion sin
last_choice, solo si puedes demostrar que, para cualquier combinación de mundo y alternativas, nunca salen las cuatro verdaderas ni las cuatro falsas.
lint comprueba la garantía sobre todas las variantes (ERROR mezcla)
con la misma regla que usan los exportadores.
2.2. Un hueco con una alternativa por mundo
El patrón habitual para cada hueco cuando hay mundos: una lista con una
Cuestion por mundo (con tres mundos A/B/C, tres cuestiones con
semantica v('A'), v('B'), v('C')). En cada variante se sortea una,
que será correcta solo si coincide con el mundo activo. El número de
aciertos entre las tres sorteadas varía de forma natural entre variantes
(0, 1, 2 o 3) sin forzarlo a mano.
Cuando una afirmación es cierta en un mundo y falsa en el otro, prefiere
una sola Cuestion con el mismo texto y semantica=v('mundoA') a dos
redacciones (una en positivo para un mundo, otra en negativo para el
otro): es más corta, y garantiza por construcción que la frase tiene
sentido sea cual sea el mundo activo. Reserva la lista de alternativas
para cuando de verdad hay varias formulaciones distintas que sortear.
Cuando no hay alternativa natural para un hueco (una afirmación siempre
verdadera o siempre falsa), déjala como Cuestion suelta; no inventes
alternativas artificiales por rellenar.
2.3. Cuántas variantes
- Sin
setup: no menos de 16-20 variantes, salvo casos excepcionales. - Con
setupaleatorio: al menos 60 en total, ajustandoexport.instances(lintcalcula el valor con un INFOsetup).lintcuenta variantes, no variantes distintas: si elsetuptiene pocos valores posibles, uninstancesalto repite variantes (ejemplosManual/ej_e_lambda.json, con 60 instancias, da solo 11 preguntas distintas). Mira la vista alumno (make amc-student).
La forma más barata de ganar variedad sin alargar ninguna variante
concreta es envolver en una lista de dos o tres redacciones cortas
equivalentes (mismo significado, misma semantica) las Cuestion que
iban sueltas: «está bien definida» / «se puede calcular» / «tiene sentido».
Una pregunta con \(2 \times 2 \times 1 \times 2 = 8\) variantes pasa a
\(2 \times 2 \times 3 \times 2 = 24\) sin que ninguna variante sea más larga.
Añadir mundos solo porque sí no es buena idea: varias preguntas centradas, una por concepto, son mejores que una megapregunta con mundos anidados.
3. Preguntas con números: setup, seed e instances
Úsalo cuando el contenido se preste a practicar un cálculo concreto y
quieras que cada alumno vea números distintos. Para preguntas
conceptuales (definiciones, propiedades, relaciones lógicas) no hace
falta setup: bastan los mundos.
3.1. setup
Es código Python, como cadena, que se ejecuta una vez por variante. Todas
las variables que define (salvo las que empiezan por _) quedan
disponibles para:
- interpolación
@{variable}en cualquier texto (enunciado,Supuesto,Cuestionyexp); - lambdas en
semanticayprecond, víans['variable'].
SETUP = r"""
import numpy as np
a = int(np.random.randint(1, 10))
b = int(np.random.randint(1, 10))
while b == a:
b = int(np.random.randint(1, 10))
cuad = (a + b) ** 2
"""
Usa np.random (no random): seed siembra numpy.random y random,
pero los scripts de la vista profe (make amc y nueva_pregunta.py) solo
siembran numpy en cada sorteo. Calcula en el setup todo lo que vaya a
aparecer en los textos (la respuesta correcta, los distractores
numéricos, cadenas enteras si hace falta): los textos solo interpolan, no
calculan. Formatea ahí también los decimales como quieras que se lean
(f"{x:.2f}", o la coma decimal como 0{,}5), porque la interpolación
escribe el valor tal cual.
La interpolación de un nombre desconocido no falla: deja @{nombre} tal
cual. lint lo detecta (ERROR interp), pero revisa igualmente los
enunciados generados. Si quieres que el alumno vea una arroba seguida de
llaves, escríbela doble: @@{x} se imprime @{x} y no es un error.
3.2. seed
Un entero fijo por pregunta, presente solo si el setup usa aleatorios.
Garantiza que la variante «1» sea siempre la misma, independientemente de
en qué orden se procesen los ficheros del banco. Un convenio cómodo: el
número de la lección.
3.3. instances
Cada recorrido de las variantes llama al setup una vez por variante; con
export.instances = N se recorre \(N\) veces con semillas distintas. La
regla:
instances = max(1, ceil(60 / variantes_por_instancia))
lint imprime el número de variantes por instancia y el instances
necesario (si los Supuesto llevan precondiciones sobre los números, el
número de variantes cambia de un sorteo a otro: lint usa la media, y el
total real puede diferir un poco). Sin setup aleatorio, no pongas
instances (o déjalo en 1).
3.4. Sortear textos, no solo números
El setup puede sortear cualquier cosa: nombres, países, décadas, frases.
np.random.choice elige de una lista de cadenas o de números (conviértelo
con str() o int() para que sea un valor normal de Python; con una lista
de tuplas o de diccionarios falla, y entonces se sortea la posición:
datos[int(np.random.randint(len(datos)))]), un diccionario resuelve las
concordancias, y las lambdas comparan cadenas igual que números:
SETUP = r"""
import numpy as np
pais = str(np.random.choice(["España", "Francia", "Portugal"]))
gentilicio = {"España": "española", "Francia": "francesa", "Portugal": "portuguesa"}[pais]
decada = int(np.random.choice([1880, 1890, 1900]))
"""
PREGUNTA = ProblemaTipo(
["Suponga que en la década de @{decada} el gobierno de @{pais} ",
[Supuesto(r"establece un arancel del 20 \% sobre las importaciones.", v('prot')),
Supuesto("suprime todos los aranceles sobre las importaciones.", v('libre'))],
Supuesto("", unoDe(v('prot'), v('libre'))),
" A corto plazo:",
[Cuestion("los bienes importados se encarecen", v('prot')),
Cuestion("los bienes importados se abaratan", v('libre'))],
[Cuestion("la industria @{gentilicio} queda protegida de la competencia exterior", v('prot')),
Cuestion("la industria @{gentilicio} queda más expuesta a la competencia exterior", v('libre'))],
Cuestion("el país es @{pais}", "lambda ns: ns['pais'] == 'Francia'")], # ejemplo de lambda sobre texto
setup=SETUP, seed=3, export={"last_choice": True, "cols": 1, "instances": 8},
nombre="L-03-PoliticaArancelaria")
La pregunta L-03-PoliticaArancelaria de la plantilla es la versión
completa de este ejemplo (sin la última cuestión, que solo ilustra la
lambda). Dos detalles de prosa: el % lleva barra (\%) y la cadena que
lo contiene es cruda (r"…"), ver la sección 7; y con opciones de una
línea o más conviene cols = 1.
3.5. No afirmes nada falso sobre los números: precond con lambda
El motor razona de forma proposicional y no conoce los valores. Si ofreces los supuestos «\(a > b\)» y «\(a < b\)» sin más, generará variantes en las que el texto dice \(a > b\) con \(a = 3\) y \(b = 7\). La solución es una precondición que solo deje pasar la variante cuando lo que afirma es cierto para los números sorteados:
[
Supuesto("$a > b$.", True, precond="lambda ns: ns['a'] > ns['b']"),
Supuesto("$a < b$.", True, precond="lambda ns: ns['a'] < ns['b']"),
],
Las variantes cuyo Supuesto no supera la precondición se descartan sin
más (no aparecen ni en la vista profe). Una Cuestion con precondición
también se puede filtrar así (precond="lambda ns: ns['k'] != 2"): en la
vista alumno la variante se descarta igual; en la vista profe la cuestión
aparece al final, en «Exclusiones», con su condición.
Las lambdas se escriben como cadena también en Python
(precond="lambda ns: …"): así la pregunta puede guardarse en JSON. Una
lambda escrita como función Python no se puede guardar.
3.6. Cuando la verdad depende de los números: semantica con lambda
Si lo que decide la corrección es un cálculo (no un mundo), la semántica
es una lambda que devuelve True o False:
Cuestion("$a^2 > b^2$", "lambda ns: ns['a'] ** 2 > ns['b'] ** 2"),
Una lambda también puede devolver una fórmula y así inyectar en las
hipótesis el estado de los números (lambda ns: v('gt') if ns['x'] > 3 else
-v('gt') en la semántica de un Supuesto axioma);
ejemplosManual/ej_e_lambda.json (el ejemplo E del cuaderno Tutorial.ipynb)
lo desarrolla. Como lint no puede mirar dentro de una
lambda, cuando un Supuesto inyecta fórmulas así no comprueba las
etiquetas huérfanas y lo dice con un INFO.
3.7. Casos especiales solo a veces
Si quieres que un caso degenerado (un vector nulo, dos números iguales)
aparezca ocasionalmente —ni nunca ni en la mitad de las variantes—, no lo
controles con una lista de alternativas (eso daría el mismo peso a cada
una): sortea una probabilidad dentro del setup, con ramas excluyentes,
y condiciona la semantica con una lambda. Esto no añade factores a la
combinatoria. Comprueba el porcentaje real contando sobre
p.por_partes(instances=…) antes de aceptar la pregunta.
3.8. Dos trampas de los setup aleatorios
- Condiciones incompletas. Si una semántica depende de una propiedad
estructural (independencia lineal, coprimalidad…), compruébala entera en
el
setup(rango, máximo común divisor), no solo el caso obvio («¿es nulo?»). Y pruébalo con muchas semillas distintas, no solo con laseeddel banco: un caso raro puede no aparecer con esa semilla y aun así ser un fallo real cuando cambieninstanceso el rango de valores. - Distractores que coinciden con la respuesta. Si el
setupcalcula un valor falso para una opción ($(a+b)^2 = @{cuad_falso}$), asegúrate de que nunca coincide con el correcto; si puede, re-sortea hasta que difieran. En general, excluye en el enunciado o en elsetuplos casos degenerados en que un distractor pasa a ser cierto (un coeficiente nulo, dos valores iguales, un factor igual a \(\pm 1\), un vector nulo).
4. Qué estilo de pregunta según el contenido
Definiciones y propiedades: pregunta puramente lógica, sin
setup, a menudo sin mundos siquiera: un enunciado y tres huecos con afirmaciones siempre verdaderas o siempre falsas (y la opción por defecto):ProblemaTipo( ["Sea $p$ un número primo mayor que 2. Entonces:", [Cuestion("$p$ es impar", True), Cuestion("$p$ es par", False)], [Cuestion("$p$ tiene exactamente dos divisores positivos", True), Cuestion("$p$ tiene exactamente un divisor positivo", False)], [Cuestion("$p + 1$ es par", True), Cuestion("$p + 1$ es primo", False)]], export={"last_choice": True}, nombre="L-01-Primos")(Solo \(2 \times 2 \times 2 = 8\) variantes: para un banco real añade redacciones alternativas o un cuarto hueco; aquí interesa la forma.)
- Resultados condicionales («si se cumple tal hipótesis, entonces…»):
mundos alternativos, uno por hipótesis relevante, y
Cuestioncuyasemanticadepende del mundo. Úsalos cuando el contenido hace una distinción real; no los fuerces cuando no la hay (una pregunta con varias cuestiones independientes y sinSupuestoes perfectamente legítima). - Cálculo numérico:
setupconnp.randome interpolación. - Una pregunta por concepto clave. No fijes de antemano cuántas preguntas saldrán de un tema: cuenta los resultados genuinamente distintos y deja que ese número decida. Forzar más produce preguntas redundantes.
- Si un concepto de un tema anterior reaparece generalizado, reutilizar el patrón de aquella pregunta es un refuerzo didáctico legítimo: antes de diseñar una pregunta nueva, mira si ya existe una sobre el concepto que se generaliza.
- No evalúes contenido de temas futuros. Que el material haga un «guiño» a
algo que se demuestra más adelante no lo convierte en evaluable con las
herramientas de este tema. Si quieres mencionarlo, hazlo en el enunciado
o en
expcomo contexto, nunca como algo que juzgar verdadero o falso. Expresiones como «volveremos sobre esto» o «más adelante veremos» son la señal de alarma.
5. Redactar cuestiones
5.1. Cada cuestión se sostiene sola
Para cada Cuestion candidata (no solo las que salen en la primera
variante), concatena mentalmente el enunciado fijo de esa variante
(incluido el Supuesto activo) con el texto de esa cuestión sola. El
resultado debe ser un texto con sentido pleno, cuya verdad o falsedad
quede determinada por lo que dice esa concatenación, nunca por lo que
«podría» haber salido en otro hueco. Como cada hueco se sortea de forma
independiente, no hay garantía de que ninguna otra cuestión en concreto
acompañe a la que revisas. Esto descarta:
- «esto ocurre porque…», «esa igualdad se cumple porque…», con un «esto» que presupone una afirmación de otro hueco. Si quieres preguntar por qué se cumple \(X\), repite \(X\) entero dentro de la cuestión;
- cualquier
expque remita a «el principio anterior» o «la misma idea» señalando a otra cuestión que puede no haberse mostrado.
Dos alternativas del mismo hueco nunca se muestran juntas; las de huecos distintos, solo a veces. Y una alternativa que es la negación literal de otra de otro hueco puede salir junto a ella y delatarla.
5.2. Símbolos definidos
Todo símbolo que aparezca en una Cuestion (\(\theta\), \(\alpha\), \(n\), \(U\)…)
debe estar definido en el enunciado de esa variante o ser notación fija
del curso que tu proyecto declare como conocida. Si el enunciado fija
objetos concretos («sean \(x = (1,2)\) e \(y = (3,1)\)») y una Cuestion
enuncia una identidad general, usa letras distintas en la identidad
(\(u\), \(v\)): con los mismos símbolos, «solo se cumple si \(x \perp y\)» tiene
doble lectura cuando esos \(x\), \(y\) concretos resultan ser ortogonales.
Cuidado también con una Cuestion que nombra un símbolo propio de un
mundo concreto: si su precond es True (lo normal), puede sortearse bajo
el mundo contrario, donde ese símbolo nunca se introdujo. Prefiere una
redacción genérica («el vector que se suma») o restringe la alternativa a
su mundo con precond.
Si varios mundos introducen parámetros análogos, usa el mismo símbolo en
todos los Supuesto: cada fórmula con ese símbolo es entonces una única
Cuestion válida en cualquier mundo, con semantica igual a la disyunción
de los mundos donde es cierta, en vez de repetir la misma fórmula varias
veces con precond distintos.
5.3. El enunciado desnudo; la justificación, en exp
No escribas «\(X\) se cumple porque \(Y\)» ni «\(X\), por el teorema \(Z\)» dentro
de una cuestión: pon «\(X\)» y mueve el porqué a exp. Además de acortar la
pregunta, evita el patrón más fácil de aprender que existe: la opción
larga es la verdadera. Es real y medible: en un banco revisado, las
opciones verdaderas medían de media entre 1,7 y 2 veces más que las falsas
porque la verdadera llevaba su «porque…» y la falsa iba desnuda.
Comprobación mecánica antes de aceptar una pregunta: media de caracteres
de las opciones verdaderas frente a las falsas; si difieren en más de un
30 %, quita las justificaciones de las verdaderas o da a las falsas una
justificación plausible pero errónea.
5.4. Gemelas: cada verdadera, con su falsa parecida
La estrategia más eficaz para que una pregunta discrimine: cada vez que escribas una cuestión verdadera, busca un distractor falso y parecido en el mismo hueco. Es como preguntar lo mismo dos veces, una bien y otra mal: distingue a quien sabe por qué algo está bien y sabe identificar el error. Y evita los distractores de relleno sin sentido: cada falsa nace de una afirmación con sentido. Gemelas típicas: papeles intercambiados (numerador/denominador), el signo o la desigualdad al revés, la fórmula con el divisor equivocado, el valor numérico con los datos cruzados.
Una afirmación numérica siempre cierta y sin gemela
($(a+b)^2 = @{cuad}$ sin su $(a+b)^2 = @{cuad_falso}$) se marca sin
comprobar nada.
5.5. Afirmaciones que no funcionan como opción
- Igualdades con ambos lados ya calculados («\(20 = 4\)»): se responden por inspección visual. Deja la identidad simbólica o pregunta por el valor de una sola cantidad.
- Ejemplos en dimensión muy baja para ilustrar un cálculo general: con dos datos, cada uno dista de la media lo mismo que el otro, un artefacto que no generaliza a tres. Reserva la dimensión baja para lo que solo tiene sentido ahí (ángulos, ortogonalidad visual).
- «Generalmente», «puede cambiar»: busca la reformulación más fuerte y determinista que capture lo mismo («deja de ser exactamente el mismo» en vez de «generalmente cambia»).
- Afirmaciones compuestas («\(n\) es par y \(n^2 + 1\) es par», mitad verdadera y mitad falsa): parte en dos o quédate con la mitad que evalúa el concepto.
- Cuestiones sobre el modo de exponer («no hace falta una demostración nueva para esto»): no son una propiedad verificable.
- Repetir una cuestión de otro hueco con relleno distinto: no es una alternativa nueva, y hace que unos alumnos vean menos alternativas realmente distintas que otros.
- Patrones fijos que se aprenden («el primer caso siempre falla», «la
tercera opción siempre es la buena»): si el
setuplo produce, rómpelo (intercambio aleatorio de papeles).
5.6. Textos cortos
Cuanto más corta sea cada pregunta, más caben por página. Antes de dar un
Supuesto o una Cuestion por buenos, pregúntate si puedes decir lo
mismo con menos palabras: fuera incisos, ejemplos redundantes con el
enunciado, explicaciones que ya van en exp, la palabra «exactamente»
cuando no aporta («\(\Leftrightarrow\)» vale por «ocurre exactamente
cuando»). El límite es que la pregunta se siga entendiendo sin
ambigüedad.
Dos columnas (export.cols = 2) ocupan menos papel que una en casi todas
las preguntas de opciones cortas; con opciones de una línea o más (lo
habitual en prosa) usa cols = 1, y mira el PDF antes de decidir. Mide con la maquetación real del examen, no con el
catálogo de revisión (que imprime el nombre de la pregunta en el
encabezado).
6. Escribir exp
exp es lo que Moodle muestra después de responder; el alumno la lee
junto a la opción, en la variante concreta que le tocó. Reglas:
- Una razón para cada mundo alcanzable. Si la
Cuestionpuede aparecer bajo varios mundos (suprecondno la restringe a uno),expdebe dar, para cada uno, una razón concreta anclada en la hipótesis de ese mundo, tanto si ahí es verdadera como si es falsa. No basta con explicar el mundo «bueno». Si la razón es genuinamente la misma en varios mundos (un argumento dimensional, por ejemplo), basta una frase que los cubra; el fallo a vigilar es cubrir solo el subconjunto donde la explicación resulta más obvia y olvidar el resto. - Sin jerga del diseño. Nunca «en el mundo \(\lambda > 0\)», «en los demás
mundos alcanzables», ni las etiquetas de
v('…'). El alumno solo ve la hipótesis escrita en su enunciado: razona con ella («si \(f\) es creciente, …; si es decreciente, …»). - Sin desvelar el diseño global. Nunca «esta opción es siempre falsa» o «en ninguno de los casos contemplados es cierto», aunque sea verdad: le permite memorizar «esta frase, descártala» en cualquier variante futura, justo lo contrario de lo que se busca al multiplicar variantes.
- Autocontenida. Nada de «es la misma identidad de antes», «el valor anterior al cuadrado», «como en el ejemplo de arriba»: la otra alternativa estaba en el mismo hueco y nunca se muestra a la vez. Repite lo que haga falta (la fórmula, el ejemplo).
- Una razón, no un nombre. «Es la fórmula del binomio» no explica nada; el porqué es justo lo que se quiere fijar. Y «nada obliga a…» donde la afirmación es rotundamente falsa debe sustituirse por la razón concreta.
- Compruébala como una demostración. «\(a \neq 0\) y \(b \neq 0\), así que
\(a + b \neq 0\)» es un razonamiento falso (\(a = -b\)). Cada
expes una afirmación matemática más que puede estar mal. - Con
setup, interpola los valores concretos (@{a}) en vez de quedarte en lo genérico; una solaexpcon@{…}sirve para todas las variantes. Si la explicación depende de una condición numérica, calcula el texto en elsetupcomo variable y ponexp="@{texto_exp}"; no escribasexpcomolambdaen JSON (llegaría literal al alumno). - Si una
expno aporta nada más allá de lo obvio, déjala vacía.
Revisa también las Cuestion con semantica False a secas: si hay
mundos, comprueba en la tabla de mundos que esa afirmación no resulte
accidentalmente cierta en alguno (dos cantidades que en general son
distintas coinciden en un mundo degenerado). Si ocurre, cambia la
semántica a la disyunción de los mundos en que es cierta, o excluye ese
mundo con precond.
7. LaTeX y Moodle: trampas conocidas
Los textos (enunciados, cuestiones y explicaciones) son LaTeX, aunque no
lleven ni una fórmula. En español puedes teclear tal cual á é í ó ú ñ ü ¿ ¡
(también en mayúscula)
y las comillas tipográficas “ ”; lo que sigue, no. lint lo detecta
(texto; el % sin barra es ERROR, el resto AVISO) y lo que no detecta lo
detecta Moodle, que es más caro.
- El símbolo
%: en LaTeX empieza un comentario. Escrito tal cual («un 25 % del valor»), se pierde el resto de la línea, el PDF de AMC sale vacío o roto y Moodle no compila. Escribe siempre\%(un 25 \% del valor); en Python, dentro de una cadena cruda (r"…") o como\\%.lintlo busca también en el texto ya interpolado, así que detecta un%que venga de una variable delsetup. Y si va junto a una fórmula, fuera de los dólares:al $5$\,\%, nuncaal $5\,\%$, porque el paquetemoodledeja dentro de la fórmula un%desnudo que MathJax toma por comentario (en papel se ve bien; solo se nota en Moodle).\,es un espacio fino, el que va entre la cifra y el símbolo. - Comillas: ni las angulares
«»(no existen en la codificación que usa el paquetemoodle: la compilación falla) ni las rectas"…"(salen como”…”, cerradas por los dos lados). Escribe``así'': dos acentos graves para abrir y dos apóstrofos para cerrar. - Grados:
$180^\circ$, nunca°(en modo matemático deja apdflatexcolgado sin error). - Fórmulas con
$…$y$$…$$; nunca\(…\)ni\[…\]directamente (qbankconvierte$$…$$en\[…\]al exportar a AMC). Separa dos expresiones en línea contiguas con un espacio ($A$ $B$, no$A$$B$). - Tablas con
arraydentro de$$…$$, con\text{}en las cabeceras, nuncatabular: el paquetemoodleno lo convierte y el alumno vería el código. - En Python, escribe los textos con LaTeX como cadenas crudas
(
r"$\mathcal{A}$") o dobla las barras; en JSON, cada\es\\. - No cites números de lección u otras referencias al material en el texto de la pregunta: las preguntas deben ser autocontenidas.
- Comprueba cada pregunta nueva en los dos destinos:
make amc(el PDF) ymake moodle(el XML, y a ser posible una vista previa en Moodle). Simakedice «ERROR en <pregunta>», la causa está en el.logque indica: busca la primera línea que empieza por!.
8. Proceso de trabajo
- Lee el material completo e identifica los resultados que merezca la pena evaluar. Cuenta cuántos son; ese es el número de preguntas.
- Para cada uno decide el estilo (mundos,
setupo cuestiones independientes) y diseña sobre papel: quéSupuesto, qué huecos, quésemanticayprecond, si hace faltasetup. - Escribe la pregunta (
nueva_pregunta.pyo el JSON) y pásalelint. Un ERROR es una pregunta rota; un AVISO hay que leerlo y decidir. - Genera la vista profe (
make amc) y repasa todas sus entradas, no la primera: enunciados bien formados, número de opciones esperado, marcas correctas en cada mundo, varias semillas si haysetup. La vista profe imprime cadaexpen cursiva junto a su cuestión. Para leer con calma,make lecturaproduce un documento por lección con un ejemplo por mundo, las alternativas agrupadas por hueco con su V/F y suexp, y las excluidas con su motivo: es el formato para el paso 7. - Pasa el checklist de abajo cuestión por cuestión.
- Si algo falla, corrige y vuelve al paso 3; no lo dejes para después.
- Antes de publicar, que la lea otra persona sin el contexto de quien la
escribió, contra el material: la segunda mirada encuentra fallos de
fondo que ni
lintni el autor ven.
8.1. Checklist
[ ]¿Cada elemento suelto va sin envolver en una lista de un elemento, y ninguna lista mezcla tipos?[ ]¿Cada hueco es un elemento-Cuestion (suelto o lista de alternativas), y hay tres huecos consecutivos +last_choice(o cuatro con garantía demostrada)?[ ]¿Coincidenombrecon el nombre del fichero, sin:ni espacios?[ ]¿Usa elsetupnp.random(norandom)? ¿Llevaseedsolo si elsetupsortea, y dainstances≥ 60 variantes, sin demasiadas repetidas en la vista alumno? ¿Sinsetup, hay ≥ 16?[ ]¿Las negaciones tienen su axiomaunoDe, o están formuladas en positivo?[ ]¿Cada cuestión, sola con el enunciado de su variante, tiene sentido pleno y verdad inequívoca? ¿Nada remite a otra alternativa?[ ]¿Todo símbolo está definido, y las identidades generales usan letras distintas de los objetos concretos?[ ]¿El enunciado afirma solo lo que elprecondgarantiza sobre los números?[ ]¿Cada verdadera numérica tiene su gemela falsa? ¿Las opciones verdaderas y falsas miden parecido?[ ]¿Los casos degenerados en que un distractor se vuelve cierto están excluidos?[ ]¿Cadaexpjustifica cada mundo alcanzable, sin jerga del diseño, sin desvelar el diseño global y sin referencias a otras alternativas?[ ]¿Todo%lleva barra (\%) y va fuera de los dólares? ¿Sin°, sin«», sin comillas rectas? ¿Fórmulas con$y tablas conarray?[ ]En prosa: ¿cada afirmación es claramente cierta en su mundo y claramente falsa en los otros, sin «depende»? ¿La hipótesis delSupuestobasta para decidirla, sin suponer conocimientos que no están en el enunciado?[ ]¿Has repasado todas las entradas de la vista profe (o de la vista de lectura) y, consetup, varias semillas?[ ]¿Has pasadomake patrones(lint --patrones) y leído sus avisos? ¿Podría un alumno acertar sin leer el enunciado, por la longitud de la frase, una palabra, una cifra o porque la gemela falsa es siempre la mayor?
Si alguna casilla falla, la pregunta no está lista.
9. El mismo ejemplo en JSON
Es lo que escribe save_problema (o nueva_pregunta.py --guardar) para el
ejemplo de la primera sección, y lo que leen lint y los scripts de
make. Las fórmulas y las lambdas van como cadenas; las barras de LaTeX
se doblan.
{
"version": "1",
"tipo": "ProblemaTipo",
"nombre": "L-01-Monotonia",
"setup": null,
"componentes": [
"Sea $f$ una función ",
[
{"tipo": "Supuesto", "enunciado": "estrictamente creciente",
"semantica": "v('crec')", "precond": "True"},
{"tipo": "Supuesto", "enunciado": "estrictamente decreciente",
"semantica": "v('decr')", "precond": "True"}
],
{"tipo": "Supuesto", "enunciado": "",
"semantica": "unoDe(v('crec'), v('decr'))", "precond": "True"},
" y sean $a < b$ dos puntos de su dominio. Entonces:",
[
{"tipo": "Cuestion", "enunciado": "$f(a) < f(b)$",
"semantica": "v('crec')", "precond": "True",
"exp": "Si $f$ crece, $a<b$ implica $f(a)<f(b)$; si decrece, $f(a)>f(b)$."},
{"tipo": "Cuestion", "enunciado": "$f(a) > f(b)$",
"semantica": "v('decr')", "precond": "True",
"exp": "Si $f$ crece, $a<b$ implica $f(a)<f(b)$; si decrece, $f(a)>f(b)$."}
],
{"tipo": "Cuestion", "enunciado": "$f(a) = f(b)$",
"semantica": "False", "precond": "True",
"exp": "Una función estrictamente monótona no repite valores."},
[
{"tipo": "Cuestion", "enunciado": "$f(b) - f(a) > 0$",
"semantica": "v('crec')", "precond": "True",
"exp": "El signo de $f(b)-f(a)$ con $a<b$ es el de la monotonía."},
{"tipo": "Cuestion", "enunciado": "$f(b) - f(a) < 0$",
"semantica": "v('decr')", "precond": "True",
"exp": "El signo de $f(b)-f(a)$ con $a<b$ es el de la monotonía."}
]
],
"export": {"last_choice": true, "cols": 2}
}
El orden de las claves da igual. Con setup, el campo es una cadena con
el código y los saltos de línea como \n; seed va al nivel superior
(save_problema lo omite cuando no hay; escribir "seed": null es
equivalente); export.instances junto a last_choice y cols. Puedes añadir claves propias de nivel superior
("_nivel": "M", "_tema": "…"): qbank las ignora, así que sirven para
tus metadatos sin romper nada.