Concatenazione efficace senza duplicati utilizzando script abbinando i dati in due colonne di tabelle diverseCiao a tutti,
È passato un po' di tempo dal mio ultimo post, e in questo lasso di tempo credo di essere diventato un po' più esperto nell'uso di JMP. Senza dubbio, usare i corsi di laurea specialistica in intelligenza artificiale (AI-LLM) in rapida espansione per autoapprendere JSL ha funzionato a meraviglia. Tuttavia, mi sto imbattendo in un problema che mi tormenta da molto tempo con JMP.
Come suggerisce il titolo, deriva dalla concatenazione di tabelle di grandi dimensioni senza duplicazione dei valori. Utilizzo JMP 18. Ho allegato due tabelle di dati di prova (ovviamente con un set di dati molto limitato e ripulito; quelle reali includono più di 100.000 righe e più di 30 colonne). Probabilmente dovrebbero essere allegate a un progetto, ops.
Vorrei spiegare il contesto (molto contesto per una domanda in definitiva molto diretta, mi scuso):
Ho un flusso di lavoro ragionevolmente sviluppato per l'analisi regolare dei dati. Ho una tabella di monitoraggio dei campioni, con gli ID dei campioni e le relative informazioni. Ho una tabella dati generale, che inserisco con le informazioni dei miei test. Le due sono collegate tramite un join virtuale, con l'ID del campione come chiave. Nella tabella delle informazioni sui campioni, ogni ID campione è univoco. Nella tabella dei dati del ciclo, ogni ID campione ha più righe di dati, ciascuna con (idealmente) un ID ciclo univoco. Ogni combinazione univoca di ID campione e ID ciclo ha dati associati (varie metriche di performance).
Un test ha più cicli: in effetti, un test è attivo e il numero del ciclo e i dati associati vengono costantemente aggiornati. Periodicamente, esporto i dati del ciclo per ciascun campione, importandoli in JMP e quindi concatenando le due tabelle: l'ultimo snapshot e la tabella dei dati in blocco. Questo processo crea moltissimi duplicati. Ad esempio, prendiamo il campione A. A nella tabella dei dati in blocco ha già ID ciclo 1 -> 1000. Nell'ultimo snapshot, A avrà dati per ID ciclo 1 -> 1200. Concatenando le mie tabelle, la tabella dei dati in blocco avrà, per il campione A, ID ciclo 1 -> 1000 *2 e 1001 -> 1200 *1.
Per il mio flusso di lavoro personale, non è l'ideale, ma va bene. Seleziono tutte le colonne, seleziono tutte le righe duplicate e le elimino. Non è l'ideale, ma va bene così.
Il problema in realtà nasce da ciò su cui ho lavorato di recente. Il modo in cui costruisco i miei dati in blocco, e la combinazione con i virtual join, rappresenta una strategia molto efficace per la filtrazione dei dati. Il mio team se n'è accorto e, con un certo impegno, sono riuscito a convincere le persone ad adottare strategie simili. Tuttavia, sono l'utente JMP più avanzato del nostro gruppo. Gli altri non hanno lo stesso tempo o impegno per imparare a gestire le complessità di JMP/JSL. Questo va bene, è una responsabilità che accolgo con piacere.
Ma voglio che abbiano accesso alla mia capacità di elaborare dati.
Ho trovato una soluzione. L'idea è di gestire la tabella dei dati in blocco come tabella esterna. La aggiornerò utilizzando un progetto di tipo amministrativo. Ho scritto script che si inizializzano automaticamente sui loro progetti, definendo funzioni per aggiornare automaticamente una copia locale incorporata del file master nei loro progetti. Funzionano perfettamente: posso richiamare correttamente uno snapshot della tabella dei dati master nei progetti di altre persone! Ottimo, ne sono molto soddisfatto. Il problema è che l'approccio più logico è impedire loro di lavorare su quello snapshot. Lo script che ho scritto eliminerà quella tabella ogni volta che si aggiorna e ne genererà una nuova identica con lo stesso nome al suo posto. Voglio che questo rimanga invariato: impedirà agli utenti di avere 30 copie della tabella master con nomi diversi in vari momenti dello snapshot. Il passo successivo è quindi aggiornare la tabella dei dati di lavoro dell'utente con i nuovi dati provenienti dalla copia master.
Sto cercando di scrivere uno script per raggiungere questo obiettivo. Ho bisogno che aggiorni la tabella di lavoro mantenendo ciò che gli utenti hanno aggiunto.
Ho provato a usare la funzione Update(), ma per quanto ne so sostituisce solo i dati esistenti con nuovi dati. Non riesco proprio a trovare un modo per far sì che aggiunga nuove righe. Join aggiunge nuove righe ma crea anche una nuova tabella. Nessuna delle due soluzioni funziona per me. L'unico modo che riesco a trovare è concatenare. Ma non voglio che gli utenti abbiano a che fare con righe duplicate. I duplicati possono anche essere piuttosto seri: le tabelle dati sono composte da centinaia di migliaia di righe e potrebbero esserci fino a 50.000 duplicati alla volta.
Il mio approccio attuale (JSL incluso) consiste nel creare colonne temporanee nella tabella master e nella tabella di lavoro. Queste concatenano l'ID campione || ID ciclo, creando una chiave univoca per una singola colonna, chiameremo keyKey. Voglio quindi creare una seconda colonna nella tabella master. Voglio che questa colonna confronti la chiave keyKey nella tabella 1 e nella tabella 2, chiameremo la colonna Selector. Se keyKey esiste in entrambe le tabelle, voglio che il valore nella colonna Selector sia 0; se esiste solo nella tabella master, voglio che il valore sia 1. Voglio quindi che lo script selezioni tutte le righe con valore 1, crei una tabella di sottoinsieme temporanea basata su tale selezione, quindi concateni tale sottoinsieme alla tabella di lavoro. Boom. Concatenazione senza duplicati. MA. Non riesco proprio a capire come confrontare keyKey tra due tabelle e usare quel confronto per generare un valore di colonna.
Sto cercando disperatamente di evitare di usare i cicli For Each Row(). Non voglio un carico di memoria così elevato per tabelle dati con dimensioni superiori a 100.000 righe.
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." );
Inoltre, se qualcuno potesse aiutarmi a capire come utilizzare le variabili stringa per l'identificazione dei nomi delle colonne (Passaggio 1, commentato perché non sono riuscito a capire come risolvere anche quello), sarebbe fantastico.
Comunque, grazie per aver letto questo gigantesco post,
Tardi
Automazione e scriptingAccesso ai datiCondivisione e comunicazione dei risultatiRe: Concatenazione efficace senza duplicati tramite scripting abbinando i dati in due colonne di tabelle diversePotresti usare Join -> crea nuova tabella -> sovrascrivi la vecchia tabella con la tabella unita appena creata se vuoi mantenere invariata la tabella originale?
Re: Concatenazione efficace senza duplicati tramite scripting abbinando i dati in due colonne di tabelle diverseSe non riesci a seguire il suggerimento di Jarmo, puoi sempre concatenare e poi eliminare i duplicati.
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: Concatenazione efficace senza duplicati tramite scripting abbinando i dati in due colonne di tabelle diverse@jthi Voglio evitare di eliminare la tabella di lavoro dell'utente. Quindi, per quanto ne so finora, la concatenazione è l'unica scelta praticabile.
@mmarchandFSLR Immagino che questa sembri sempre più la risposta più semplice, anche se sono titubante riguardo al potenziale rallentamento di molti duplicati. Sono solo scioccato dal fatto che in JMP sembri impossibile concatenare dati non duplicati, sembra una funzione così complessa da non essere presente...
Re: Concatenazione efficace senza duplicati utilizzando script abbinando i dati in due colonne di tabelle diverseEcco perché ho suggerito di eseguire join e poi di utilizzare la tabella unita per "aggiornare" la tabella originale.
JMP al momento non dispone di una funzionalità di upsert (aggiornamento + inserimento) adeguata, ma è possibile crearne una propria. Potrebbe essere un po' complicato crearne una efficiente... Inoltre, hai provato a vedere se riscontri problemi con la selezione dei duplicati? O sarebbe solo un'ottimizzazione prematura per cercare di evitarlo?
Re: Concatenazione efficace senza duplicati tramite scripting abbinando i dati in due colonne di tabelle diverseNo, hai ragione. È stata un'ottimizzazione prematura, e sì, concatenare ed eliminare i duplicati funziona con il nostro set di dati, nonostante crei diverse centinaia di migliaia di duplicati da eliminare. Mi sembra un modo orribile per ottenere quella che a mio parere è una funzione piuttosto elementare.
Anche se capisco il concetto alla base dell'approccio join, sicuramente è ancora bloccato da Update() che non aggiunge nuove righe. A meno che non abbia capito male, potresti chiarire?
Di seguito il codice finale ripulito per chiunque fosse curioso:
// ** 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: Concatenazione efficace senza duplicati utilizzando script abbinando i dati in due colonne di tabelle diverseNon è bloccato da nulla. La mia idea era di usare Join per creare una nuova tabella con tutte le righe e poi usarla per sovrascrivere tutti i valori della vecchia tabella. Questo può essere fatto, ad esempio, usando l'indicizzazione delle tabelle dati.
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);
Potresti anche fare qualcosa del genere, ma fai attenzione a come JMP può interferire con gli script/variabili delle tabelle
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);