Dependencias jerárquicas
Esta es la característica estrella de TDC. Los generadores de datos falsos comunes
llenan cada campo de forma independiente. TDC puede enlazar
secuencias en una relación padre → hijo: los
valores del hijo se calculan solo en las filas donde el padre tomó cierto valor, y
cualquier porcentaje dentro del hijo se mide contra el tamaño de ese subconjunto
filtrado, no contra el count completo.
Eso es lo que permite modelar distribuciones dependientes reales — «nombres masculinos solo para hombres», «el diagnóstico depende del sexo», «los niños no tienen cónyuge» — en una sola descripción declarativa.
Las salidas de ejemplo que siguen son ilustrativas. Los valores exactos que emite un generador pueden cambiar entre versiones del core y entre semillas; lo que la característica garantiza son los conteos y las reglas estructurales (qué filas se llenan y cuáles quedan vacías).
- filas donde el padre eligió A — el hijo de abajo siempre es 1, 2 o 3
- filas donde el padre eligió B — el hijo de abajo siempre es 7 u 8
El problema: los campos independientes producen pares imposibles
Para ver por qué importa parent, conviene mirar qué
pasa sin él. Dos secuencias text, país y ciudad,
declaradas de forma independiente:
<env count="8" seed="demo">
<sequence name="Country"><gen type="text" value="Russia,France" percent="50,50"/></sequence>
<sequence name="City"><gen type="text" value="Moscow,Paris" percent="50,50"/></sequence>
</env>
<block><line><data>${{Country}}: ${{City}}</data></line></block>
Russia: Moscow Russia: Moscow France: Paris France: Paris Russia: Moscow France: Paris Russia: Paris France: Moscow
Cada campo tira su propio dado. Las dos últimas filas son el problema: Russia: Paris y France: Moscow — París en Rusia, Moscú en Francia. Para una prueba que verifica que «la ciudad pertenece al país», esos datos son basura, y entre más filas, más pares imposibles.
La solución: parent
Se le da a cada país su propio generador de ciudades, filtrado con
parent="Country.Valor". Ahora la ciudad se elige solo en las filas del país que
corresponde:
<env count="8" seed="demo">
<sequence name="Country"><gen type="text" value="Russia,France" percent="50,50"/></sequence>
<sequence name="CityRU" parent="Country.Russia"><gen type="text" value="Moscow,Kazan"/></sequence>
<sequence name="CityFR" parent="Country.France"><gen type="text" value="Paris,Lyon"/></sequence>
</env>
<block><line><data>${{Country}}: ${{CityRU}}${{CityFR}}</data></line></block>
Russia: Moscow Russia: Moscow France: Lyon France: Lyon Russia: Kazan France: Paris Russia: Kazan France: Paris
La columna Country es la misma (mismo seed),
pero ahora la ciudad siempre es consistente: Rusia recibe únicamente Moscow/Kazan,
Francia únicamente Paris/Lyon. Los pares imposibles desaparecieron. En cada fila
está activo exactamente uno de los dos generadores de ciudad y el otro queda vacío, así
que ${{CityRU}}${{CityFR}} produce una sola ciudad.
Cómo funciona
- Se declara el padre con una distribución
(
textconpercent,templateo cualquier otro tipo). - Se declara el hijo con
parent="Parent.Value". - El hijo se materializa solo en las filas donde
Parent == Value. En las demás filas su valor queda indefinido — una cadena vacía en la interpolación. - Cualquier porcentaje dentro del hijo se mide contra el número de filas activas, es decir, contra el subconjunto filtrado.
Nombres según el género
El caso clásico: los hombres necesitan nombres masculinos y las mujeres, nombres
femeninos. Un único generador de nombres compartido no puede hacerlo — no conoce el
género de la fila. Dos generadores de nombres, cada uno bajo su propio parent, sí:
<env count="8" seed="demo">
<sequence name="Gender"><gen type="text" value="Hombre,Mujer" percent="60,40"/></sequence>
<sequence name="MaleName" parent="Gender.Hombre"><gen type="template" value="person.male.firstName"/></sequence>
<sequence name="FemaleName" parent="Gender.Mujer"><gen type="template" value="person.female.firstName"/></sequence>
</env>
<block><line><data>${{Gender}}: ${{MaleName}}${{FemaleName}}</data></line></block>
Mujer: Alexis Hombre: Bruno Hombre: Khari Hombre: Adriel Hombre: Wayne Hombre: Callan Mujer: Alena Mujer: Naomi
Genderconcount="8"produce 5Hombre+ 3Mujer(60/40 de ocho, redondeado con el método del resto mayor — veapercent). Concount="100"serían exactamente 60 + 40.MaleNamese llena solo en las filas masculinas (la plantillaperson.male.firstNametoma valores del diccionario masculino); en las filas femeninas queda vacío.FemaleName, de forma simétrica, se llena solo en las filas femeninas.- Concatenar
${{MaleName}}${{FemaleName}}deja exactamente un nombre por fila, y siempre corresponde al género — nuncaHombre: Alena.
Probabilidad dentro de un subconjunto
«30 % de los aficionados» — ¿30 % de quiénes? Si solo los rusos pueden ser
aficionados y usted cuenta el 30 % de todo el mundo, la proporción real de aficionados
rusos se reduce a la mitad. Ponga el percent en el hijo y se
aplicará a las filas filtradas:
<env count="100" seed="demo">
<sequence name="Country"><gen type="text" value="RU,US" percent="50,50"/></sequence>
<sequence name="FootballFan" parent="Country.RU"><gen type="text" value="Yes,No" percent="30,70"/></sequence>
</env>
<block><line><data>${{Country}} -> [${{FootballFan}}]</data></line></block>
RU -> [Yes] RU -> [Yes] US -> [] RU -> [No] RU -> [No] US -> [] US -> [] RU -> [No] RU -> [No] RU -> [No]
En las filas US, FootballFan queda vacío — el generador nunca se dispara ahí.
Contando las 100 filas, columna por columna:
| Qué | Resultado |
|---|---|
Country | 50 RU + 50 US |
FootballFan entre las filas RU | 15 Yes + 35 No |
FootballFan entre las filas US | 50 vacías |
El reparto 30/70 se midió contra las 50 filas RU — «el 30 % de los rusos son
aficionados», no «30 de las 100 filas» (lo cual daría 30 Yes).
Variación — otro reparto, el mismo subconjunto
No se cambia nada salvo la máscara del hijo, que pasa a percent="50,50":
<sequence name="FootballFan" parent="Country.RU"><gen type="text" value="Yes,No" percent="50,50"/></sequence>
FootballFan entre las filas RU: 25 Yes + 25 No FootballFan entre las filas US: 50 vacías
Ahora se obtienen exactamente 25 Yes + 25 No entre esas mismas 50 filas RU — la
mitad del subconjunto, no la mitad de count. Las filas US siguen vacías. Por eso el
porcentaje vive en el hijo: siempre se escala al segmento filtrado por el padre.
Orden de declaración
Un padre debe declararse antes que su hijo en el documento. Si se declaran al revés, el render falla de inmediato:
<env count="8" seed="demo">
<sequence name="City" parent="Country.Russia"><gen type="text" value="Moscow,Kazan"/></sequence>
<sequence name="Country"><gen type="text" value="Russia,France" percent="50,50"/></sequence>
</env>
Error: parent sequence "Country" is not declared before this sequence
El error indica la línea y la columna del problema. No se admiten dependencias cíclicas ni referencias hacia adelante — el grafo de dependencias se resuelve de arriba hacia abajo, así que los padres siempre van primero.
parent sin valor
parent="Parent" — sin punto ni valor — significa «cualquier fila donde el padre
tenga algún valor», sin importar cuál. Rara vez se necesita en el primer nivel (un
padre de nivel superior siempre tiene valor), pero es la herramienta para cadenas más
profundas, donde una secuencia intermedia está a su vez filtrada y usted quiere un nieto
solo donde ese nivel intermedio se disparó.
Aquí Country elige las filas US, USCity llena solo esas filas, y USZip debe
aparecer dondequiera que haya una ciudad de EE. UU. — cualquiera de ellas —, así que
usa la forma sin valor:
<env count="8" seed="demo">
<sequence name="Country"><gen type="text" value="US,UK" percent="50,50"/></sequence>
<sequence name="USCity" parent="Country.US"><gen type="text" value="New York,Chicago"/></sequence>
<sequence name="USZip" parent="USCity"><gen type="regex" value="[0-9]{5}"/></sequence>
</env>
<block><line><data>${{Country}} | ${{USCity}} ${{USZip}}</data></line></block>
US | New York 10021 US | Chicago 60614 UK | UK | US | New York 10021 UK | US | Chicago 60614 UK |
USZip se dispara en cada fila US y en ninguna otra — parent="USCity.New York" lo
habría restringido únicamente a Nueva York. La forma sin valor dice «hereda el filtro
del padre, no agregues el mío», que es justo lo que se quiere para un campo que cuelga
de lo que sea que haya producido el padre.
Interacción con if
Las expresiones if se evalúan contra los valores de la
fila actual. En una fila filtrada, el valor del hijo queda indefinido — se trata como
vacío, es decir, como falso. Así que una condición sobre una columna hija excluye
automáticamente las filas donde ese hijo nunca se disparó, sin ninguna verificación
explícita del padre:
<env count="8" seed="demo">
<sequence name="Country"><gen type="text" value="RU,US" percent="50,50"/></sequence>
<sequence name="FootballFan" parent="Country.RU"><gen type="text" value="Yes,No" percent="30,70"/></sequence>
</env>
<block><line>
<data>${{Country}} ${{FootballFan}}</data>
<data if="FootballFan == Yes"> BUY</data>
</line></block>
RU Yes BUY RU No US US RU No US RU No US
Ticket se renderiza solo donde FootballFan == Yes. Las filas US tienen un
FootballFan vacío, que la comparación lee como falso, así que pasan de largo sin
Ticket — y nunca hizo falta escribir Country == RU en la condición.
Un árbol en los datos, no en la configuración
parent relaciona una secuencia con otra. Casi con la misma frecuencia aparece otra
tarea: un registro que apunta a otro registro del mismo tipo. Un empleado cuyo jefe es
un empleado, un comentario que responde a un comentario, una categoría dentro de una
categoría.
Eso es una columna, no una construcción, y toda la dificultad está en una palabra: ciclos. Una cadena de jefes que se cierra sobre sí misma colgará a lo que intente dibujarla, y volver a sortear no lo arregla, porque el problema está en la forma y no en ningún valor concreto.
La solución es aritmética y no necesita nada nuevo. Que cada registro apunte a un id menor que el suyo:
<env count="10" seed="tree" local="en">
<sequence name="Id"><gen type="increment" value="1"/></sequence>
<sequence name="Back"><gen type="number" value="1..4"/></sequence>
<sequence name="ParentId"><compute><result>
<choose>
<when>
<test><less_than><subtract><to_number><field name="Id"/></to_number><to_number><field name="Back"/></to_number></subtract><int v="1"/></less_than></test>
<then>
<choose>
<when><test><equals><to_number><field name="Id"/></to_number><int v="1"/></equals></test><then><int v="0"/></then></when>
<otherwise><int v="1"/></otherwise>
</choose>
</then>
</when>
<otherwise><subtract><to_number><field name="Id"/></to_number><to_number><field name="Back"/></to_number></subtract></otherwise>
</choose>
</result></compute></sequence>
<sequence name="Author"><gen type="template" value="person.lastName"/></sequence>
</env>
<block>
<line><data>${{Id}},${{ParentId}},${{Author}}</data></line>
</block>
1,0,Smith 2,1,Jones 3,1,Miller 4,1,Garcia 5,3,Davis 6,5,Williams 7,5,Brown 8,7,Johnson 9,7,Martinez 10,7,Rodriguez
Se lee como id, parent_id, autor. Back es cuántas filas hacia arriba se engancha este
registro — de una a cuatro — y las dos ramas de <choose> resuelven el principio del
archivo: el registro 1 recibe 0, la raíz, y todo lo que llegaría más arriba se engancha
a la raíz.
Lo que eso garantiza por construcción, no por suerte:
- Sin ciclos. Toda flecha apunta a un número menor, así que seguirlas siempre termina.
- Una sola raíz. Solo el registro 1 no tiene padre.
- Una forma viva. El registro 1 tiene aquí tres hijos y el 2 ninguno, porque
Backse sortea por fila. Amplíelo a1..20para un árbol plano y ancho; redúzcalo a1..2para uno profundo y estrecho.
La misma columna sirve para un organigrama, un árbol de comentarios, una lista de
materiales o categorías anidadas. Lo que los registros dicen es otra cuestión: el texto
de un comentario es simplemente text.paragraph y no tiene
que formar una conversación coherente para que el árbol sea un árbol válido.
Véase también
- Secuencias y
parent— la mecánica detrás del filtro. - Determinismo y proporciones — cómo
seedypercentse mantienen exactos. - Datos coherentes y relacionales — padre → hijo mediante búsqueda por nombre.