Concatenación efectiva sin duplicados mediante scripts haciendo coincidir datos en dos columnas de tablas diferentesHola a todos,
Ha pasado un tiempo desde mi última publicación, y creo que me he vuelto un poco más competente con JMP. Sin vergüenza alguna, usar los crecientes LLM de IA para autoaprender JSL ha funcionado de maravilla. Sin embargo, me estoy encontrando con un problema que me ha estado molestando durante mucho tiempo con JMP.
Como indica el título, se deriva de la concatenación de tablas grandes sin duplicar valores. Estoy usando JMP 18. He adjuntado dos tablas de datos de prueba (con un conjunto de datos obviamente muy limitado y depurado; las reales incluyen más de 100 000 filas y más de 30 columnas). Probablemente deberían adjuntarse a un proyecto, ¡vaya!
Permítanme explicarles los antecedentes (mucho contexto para una pregunta muy directa, disculpas):
Cuento con un flujo de trabajo bastante desarrollado para el análisis regular de datos. Dispongo de una tabla de seguimiento de muestras con sus identificadores y su información asociada. Tengo una tabla de datos generales que relleno con la información de mis pruebas. Ambas están vinculadas mediante una unión virtual, con el identificador de muestra como clave. En la tabla de información de muestras, cada identificador de muestra es único. En la tabla de datos de ciclo, cada identificador de muestra tiene varias filas de datos, cada una con (idealmente) un identificador de ciclo único. Cada combinación única de identificador de muestra e identificador de ciclo tiene datos asociados (diversas métricas de rendimiento).
Una prueba tiene varios ciclos; de hecho, una prueba está activa y el número de ciclo y sus datos asociados se actualizan constantemente. Periódicamente, exporto los datos del ciclo de cada muestra, los importo a JMP y luego concateno las dos tablas: la última instantánea y la tabla de datos masivos. Este proceso crea muchos duplicados. Por ejemplo, tomemos la muestra A. En la tabla de datos masivos, A ya tiene el ID de ciclo 1 -> 1000. En la última instantánea, A tendrá datos para el ID de ciclo 1 -> 1200. Concateno las tablas y la tabla de datos masivos tendrá, para la muestra A, el ID de ciclo 1 -> 1000 *2 y 1001 -> 1200 *1.
Para mi flujo de trabajo personal, esto no es ideal, pero está bien. Selecciono todas las columnas, selecciono todas las filas duplicadas y las borro. No es óptimo, pero da igual.
El problema, en realidad, surge de lo que he estado trabajando últimamente. La forma en que construyo mis datos masivos y la combinación con uniones virtuales es una estrategia muy eficaz para el filtrado de datos. Mi equipo lo ha notado y, con esfuerzo, he convencido a otros para que adopten estrategias similares. Sin embargo, soy el usuario más avanzado de JMP de nuestro grupo. Los demás no tienen el mismo tiempo ni dedicación para aprender a manejar las complejidades de JMP/JSL. No hay problema, es una responsabilidad que acepto.
Pero quiero que tengan acceso a mi capacidad de procesar datos.
He encontrado una solución. La idea es mantener la tabla de datos masivos como una tabla externa. La actualizaré mediante un proyecto similar a un administrador. He creado scripts que se inicializan automáticamente en sus proyectos y definen funciones para actualizar automáticamente una copia local incrustada del archivo maestro en sus proyectos. Funcionan correctamente: puedo acceder a una instantánea de la tabla de datos maestros en los proyectos de otros usuarios. ¡Genial, estoy muy contento! El problema es que lo más lógico es no permitirles trabajar en esa instantánea. El script que he creado eliminará esa tabla cada vez que se actualice y generará una nueva tabla idéntica con el mismo nombre en su lugar. Quiero que esto se mantenga así: evitará que los usuarios tengan 30 copias de la tabla maestra en diferentes momentos de la instantánea con diferentes nombres. El siguiente paso es actualizar la tabla de datos de trabajo del usuario con los nuevos datos de la copia maestra.
Estoy intentando escribir un script para lograrlo. Necesito que actualice la tabla de trabajo y conserve lo que los usuarios hayan añadido.
He intentado usar la función Update(), pero, por lo que he podido averiguar, solo reemplaza los datos existentes con datos nuevos. No encuentro la manera de que agregue nuevas filas. La función "Unir" agrega nuevas filas, pero también crea una nueva tabla. Ninguna de las soluciones me funciona. La única forma que encuentro es concatenar. Pero no quiero que los usuarios tengan que lidiar con filas duplicadas. Los duplicados también pueden ser bastante graves: las tablas de datos tienen cientos de miles de filas y podría haber hasta 50 000 duplicados a la vez.
Mi enfoque actual (incluido JSL) consiste en crear columnas temporales en las tablas maestra y de trabajo. Estas concatenan el ID de muestra || ID de ciclo, creando una clave de columna única, llamada keyKey. Luego, quiero crear una segunda columna en la tabla maestra. Quiero que esta columna compare keyKey de las tablas 1 y 2, llamada la columna Selector. Si keyKey existe en ambas tablas, el valor de la columna Selector debe ser 0; si solo existe en la tabla maestra, debe ser 1. Después, quiero que el script seleccione todas las filas con el valor 1, cree una tabla de subconjunto temporal basada en esa selección y luego concatene ese subconjunto a la tabla de trabajo. ¡Genial! Concatenación sin duplicados. Pero, por más que me esfuerce, no logro entender cómo comparar keyKey entre dos tablas y usar esa comparación para generar un valor de columna.
Estoy intentando evitar desesperadamente el uso de bucles For Each Row(). No quiero esa carga de memoria para tablas de datos de más de 100 000 filas.
Names Default To Here(1);
// --- Configuration ---
workingTableName = "Working_Data_Test";
readOnlyMasterName = "Master_Data_Test";
keyColA = "Cell ID";
keyColB = "Cycle ID";
// --- Get Handles & Perform Error Checks ---
dtWorking = Data Table( workingTableName );
dtMaster = Data Table( readOnlyMasterName );
If( Is Empty( dtWorking ) | Is Empty( dtMaster ), Throw("Ensure both tables are open.") );
// --- STEP 1: Update existing rows---
//Print( "Step 1: Updating existing rows..." );
//dtWorking << Update(
// With( dtMaster ),
// Match Columns( :Name(keyCols[1]) = :Name(keyCols[1]), :Name(keyCols[2]) = :Name(keyCols[2]) )
//);
// --- STEP 2: Find new rows using a temporary concatenated key ---
Print( "Step 2: Finding new rows using explicit For Each Row loop..." );
// Add the blank temporary key columns to both tables first.
dtWorking << New Column( "~temp_key~", Character );
dtMaster << New Column( "~temp_key~", Character );
// Populate the key for the Working Table
For Each Row( dtWorking,
:Name("~temp_key~") = :Name("Cell ID") || "|" || Char(:Name("Cycle ID"));
);
// Populate the key for the Master Table
For Each Row( dtMaster,
:Name("~temp_key~") = :Name("Cell ID") || "|" || Char(:Name("Cycle ID"));
);
// Create a fast lookup list of all temporary keys in the working table.
workingKeys = Associative Array( dtWorking:Name("~temp_key~") );
//Add a new "flag" column to the master table.
dtMaster << New Column( "~is_match~", Numeric
//Set its value using a formula. The formula returns 0 if the key is new, 1 if it's a match.
Column( dtMaster, "~is_match~" ) << Set Formula(
If( Is Missing( workingKeys[ :Name("~temp_key~") ] ),
0, // Not found in workingKeys -> It's a new row
1 // Found in workingKeys -> It's an existing row
)
);
// --- STEP 3: Create a new temporary table from the selected rows ---
If( N Rows( dtMaster << Get Selected Rows ) > 0,
Print( "Found " || Char( N Rows( dtMaster << Get Selected Rows ) ) || " new rows. Creating temporary table..." );
dtTempNewRows = dtMaster << Subset(
Selected Rows( 1 ),
Output Table Name( "Temp New Rows" )
);
dtTempNewRows << Show Window(1);
,
Print("No new rows found to add.");
);
// --- STEP 4: Clean up temporary columns ---
//Try( dtWorking << Delete Columns( "~temp_key~" ) );
//Try( dtMaster << Delete Columns( "~temp_key~" ) );
//dtMaster << Clear Select;
Print( "Test complete." );
Además, si alguien pudiera ayudarme a descubrir cómo usar variables de cadena para identificar nombres de columnas (Paso 1, comentado porque tampoco pude descubrir cómo solucionarlo), sería genial.
De todos modos, gracias por leer esta publicación gigantesca.
Tarde
Automatización y scriptingAcceso a datosCompartir y comunicar resultadosRe: Concatenación efectiva sin duplicados mediante scripts haciendo coincidir datos en dos columnas de tablas diferentes¿Podrías utilizar Unir -> crear nueva tabla -> sobrescribir la tabla antigua con la tabla unida recién creada si deseas mantener la misma tabla original?
Re: Concatenación efectiva sin duplicados mediante scripts haciendo coincidir datos en dos columnas de tablas diferentesSi no puedes seguir la sugerencia de Jarmo, siempre puedes concatenar y luego eliminar los duplicados.
Names Default To Here( 1 );
dt = Data Table( "Master_Data_Test" );
dt2 = Data Table( "Working_Data_Test" );
dt << Concatenate( dt2, Append to first table );
dt << Select Duplicate Rows( Match() );
If( Length( dt << Get Selected Rows ) > 0, //In case no duplicate rows exist; don't want to delete everything!
dt << Delete Rows()
);
Re: Concatenación efectiva sin duplicados mediante scripts haciendo coincidir datos en dos columnas de tablas diferentes@jthi Quiero evitar eliminar la tabla de trabajo del usuario. Por lo tanto, la concatenación es la única opción viable, según tengo entendido.
@mmarchandFSLR Supongo que esa parece cada vez más la solución más sencilla, aunque dudo de que muchos duplicados puedan ralentizar el proceso. Me sorprende que parezca imposible concatenar datos no duplicados en JMP; parece una función enorme que falta...
Re: Concatenación efectiva sin duplicados mediante scripts haciendo coincidir datos en dos columnas de tablas diferentesPor eso sugerí realizar una unión y luego usar esa tabla unida para "actualizar" la tabla original.
JMP actualmente no cuenta con la función upsert (actualizar + insertar) adecuada, pero puedes crear la tuya. Sin embargo, crear una eficiente podría ser un poco complicado... Además, ¿has probado si tienes algún problema con la selección duplicada? ¿O se trata simplemente de una optimización prematura para intentar evitarla?
Re: Concatenación efectiva sin duplicados mediante scripts haciendo coincidir datos en dos columnas de tablas diferentesNo, tienes razón. Fue una optimización prematura, y sí, concatenar y eliminar duplicados funciona con nuestro conjunto de datos, a pesar de crear cientos de miles de duplicados para eliminar. Simplemente parece una forma horrible de lograr lo que, en mi opinión, es una función bastante básica.
Aunque entiendo la idea detrás del enfoque de unión, seguramente sigue bloqueado porque Update() no agrega nuevas filas. A menos que esté malinterpretando algo, ¿podrías aclararlo?
A continuación el código final limpiado para cualquier persona curiosa:
// ** Working table update script **
// Includes lockfile check to prevent reading a mid-update master table.
//V1.1 Includes ability to deal sensibly with changing timestamps while preserving deletion of duplicates
Names Default To Here(1);
// --- Configuration ---
workingTableName = "Test_WorkingTable"; //Change this to the EXACT name of the table you want to update
keyColA = "Sample ID";
keyColB = "Test ID";
// --- Path Definitions ---
// Build the path to the master file and lock file on the shared drive.
homeDir = Get Environment Variable( "USERPROFILE" );
If( Is Empty( homeDir ), homeDir = Get EnvironmentVariable( "HOME" ) );
sharePointRelativePath = "\OUR\INTERNAL\FILEPATH\";
masterFilePath = homeDir || sharePointRelativePath || "Data_File_Name.jmp";
lockFile = homeDir || sharePointRelativePath || "lockfile.txt";
// --- 1. Check for Lock File ---
// If the lockfile exists, stop all execution immediately.
If( File Exists( lockFile ),
Print( "UPDATE FAILED: The master data file is currently locked by an admin for updates." );
Print( "Please try again later." );
Stop(); // Halt the script
);
// --- 2. Get Handle to User's Working Table ---
dtWorking = Data Table( workingTableName );
If( Is Empty( dtWorking ),
Throw( "UPDATE ERROR: Your working table '" || workingTableName || "' could not be found. Please ensure it is open." )
);
// --- 3. Open a Fresh, Unlinked Copy of the Master Data ---
dtMaster = Open( masterFilePath, invisible );
If( Is Empty( dtMaster ), Throw("Could not open master data file from shared drive.") );
// ---3.5. Deal with the timestamp table variable. It will return an empty string if it doesn't exist, and if it does exist, delete the variable.---
// A. Read the timestamp from the master table into a script variable.
newTimestamp = dtMaster << Get Table Variable( "Last Updated" );
// If the variable was found, print it to the log.
If( !Is Empty( newTimestamp ),
Print( "--- Master data was last updated on: " || newTimestamp || " ---" );
,
Print( "--- This master data does not contain a 'Last Updated' timestamp. ---" );
);
// B. Delete the variable from the master table so it's a clean data source.
// We wrap in Try() in case an old master file doesn't have the variable.
Try( dtMaster << Delete Table Variable( "Last Updated" ) );
// --- 4. Concatenate the Clean Master to the Working Table ---
Print("Step 1: Appending master data to working data...");
dtWorking << Concatenate(
dtMaster,
Append to First Table
);
Close( dtMaster, NoSave ); // The fresh master copy is no longer needed.
// --- 5. Find and Delete Duplicate Rows ---
Print("Step 2: Identifying and removing duplicate rows...");
dtWorking << Select Duplicate Rows(
Match( As Column(keyColA), As Column(keyColB) )
);
nDuplicates = N Rows( dtWorking << Get Selected Rows );
If( nDuplicates > 0,
Print("Found and deleting " || Char(nDuplicates) || " duplicate rows.");
dtWorking << Delete Rows;
);
dtWorking << Clear Select;
// 6. If a new timestamp was found, set it on the working table.
If( !Is Empty( newTimestamp ),
dtWorking << Set Table Variable( "Last Updated", newTimestamp );
Print( "Working table timestamp updated to: " || newTimestamp );
);
Print( "Process complete. Final table has " || Char(N Rows(dtWorking)) || " rows." );
Window( workingTableName ) << Bring Window to Front;
Re: Concatenación efectiva sin duplicados mediante scripts haciendo coincidir datos en dos columnas de tablas diferentesNo está bloqueado por nada. Mi idea era usar "Join" para crear una nueva tabla con todas las filas y luego usarla para sobrescribir todos los valores de la tabla anterior. Esto se puede hacer, por ejemplo, usando subscripts de tablas de datos.
Names Default To Here(1);
dt_master = Open("$DOWNLOADS/Master_Data_Test.jmp");
dt_working = Open("$DOWNLOADS/Working_Data_Test.jmp");
dt_new = dt_master << Join(
With(dt_working),
Merge Same Name Columns,
Match Flag(0),
By Matching Columns(:Cycle ID = :Cycle ID, :Cell ID = :Cell ID),
Drop multiples(0, 0),
Include Nonmatches(1, 1),
Preserve main table order(1),
Output Table("updated master"),
private
);
dt_master << Delete Rows(1::NRows(dt_master));
dt_master << Add Rows(N Rows(dt_new));
dt_master[0,0] = dt_new[0,0];
Close(dt_new, no save);
También puedes hacer algo como esto, pero ten cuidado con la forma en que JMP puede interferir con los scripts/variables de la tabla.
Names Default To Here(1);
dt_master = Open("$DOWNLOADS/Master_Data_Test.jmp");
dt_working = Open("$DOWNLOADS/Working_Data_Test.jmp");
// JMP can mess with your Source script if you have one
has_update_script = Contains(dt_master << Get Table Script Names, "Update") > 0;
If(has_update_script,
dt << Rename Table Script("Update", "Original_Update");
);
// possibly unnecessary Update
dt_master << Update(
With(dt_working),
Match Columns(:Cycle ID = :Cycle ID, :Cell ID = :Cell ID)
);
dt_master << Delete Scripts("Source");
If(has_update_script,
Try(dt_master << Rename Table Script("Original_Update", "Source"));
);
dt_joined = dt_master << Join(
With(dt_working),
Merge Same Name Columns,
Match Flag(1),
Copy second table formula(0),
By Matching Columns(:Cycle ID = :Cycle ID, :Cell ID = :Cell ID),
Drop multiples(0, 0),
Include Nonmatches(0, 1),
Preserve main table order(1),
Output Table("joined"),
private
);
old_rows = Where(dt_joined, :Match Flag == 3); // 3 = "Both"
dt_joined << Delete Columns(1);
dt_joined << Delete Rows(old_rows);
dt_master << Concatenate(
dt_joined,
"Append to first table"
);
Close(dt_joined, no save);
wait(0);