Concaténation efficace sans doublons à l'aide de scripts en faisant correspondre les données de deux colonnes dans des tables différentesSalut tout le monde,
Cela fait un moment que je n'ai pas publié de message, et depuis, je pense avoir gagné en maîtrise de JMP. Utiliser les LLM en IA, en plein essor, pour apprendre le JSL en autodidacte fonctionne à merveille. Cependant, je rencontre un problème qui me taraude depuis très longtemps avec JMP.
Comme le titre l'indique, cela résulte de la concaténation de grandes tables sans duplication de valeurs. J'utilise JMP 18. J'ai joint deux tables de données de test (avec un ensemble de données évidemment très limité et épuré ; les vraies tables contiennent plus de 100 000 lignes et plus de 30 colonnes). Celles-ci devraient probablement être jointes à un projet, oups.
Laissez-moi vous expliquer le contexte (beaucoup de contexte pour une question finalement très directe, toutes mes excuses) :
J'ai un workflow relativement développé pour l'analyse régulière des données. J'ai une table de suivi des échantillons, avec leurs identifiants et leurs informations associées. J'ai une table de données générales, que j'alimente avec les informations de mes tests. Les deux sont liées par une jointure virtuelle, avec l'identifiant de l'échantillon comme clé. Dans la table d'informations sur les échantillons, chaque identifiant d'échantillon est unique. Dans la table de données de cycle, chaque identifiant d'échantillon comporte plusieurs lignes de données, chacune avec (idéalement) un identifiant de cycle unique. Chaque combinaison unique d'identifiant d'échantillon et d'identifiant de cycle est associée à des données (diverses mesures de performance).
Un test comporte plusieurs cycles. En fait, un test est en direct et le numéro de cycle ainsi que les données associées sont constamment mis à jour. J'exporte régulièrement les données de cycle de chaque échantillon, les importe dans JMP, puis je concatène les deux tables : la dernière capture instantanée et la table de données globales. Ce processus crée de nombreux doublons. Prenons l'exemple de l'échantillon A. Dans la table de données globales, l'échantillon A a déjà l'ID de cycle 1 -> 1000. Dans la dernière capture instantanée, l'échantillon A contiendra les données de l'ID de cycle 1 -> 1200. Je concatène mes tables et la table de données globales aura, pour l'échantillon A, l'ID de cycle 1 -> 1000 *2 et 1001 -> 1200 *1.
Pour mon flux de travail personnel, ce n'est pas idéal, mais ça va. Je sélectionne toutes les colonnes, puis toutes les lignes en double et je les supprime. Ce n'est pas optimal, mais tant pis.
Le problème vient en fait de mon travail récent. Ma façon de construire mes données massives et la combinaison avec des jointures virtuelles constituent une stratégie très efficace pour filtrer les données. Mon équipe l'a remarqué et, à force de travail, j'ai réussi à convaincre les autres d'adopter des stratégies similaires. Cependant, je suis l'utilisateur JMP le plus expérimenté de notre groupe. Les autres n'ont pas le même temps ni la même motivation pour apprendre à gérer les subtilités de JMP/JSL. C'est normal, c'est une responsabilité que j'accepte avec plaisir.
Mais je veux qu’ils aient accès à ma capacité à traiter les données.
J'ai trouvé une solution. L'idée est de gérer la table de données en masse comme une table externe. Je la mettrai à jour via un projet administrateur. J'ai écrit des scripts qui s'initialisent automatiquement sur leurs projets, définissant des fonctions pour actualiser automatiquement une copie locale intégrée du fichier maître dans leurs projets. Cela fonctionne parfaitement : je peux appeler un instantané de la table de données maître dans les projets d'autres utilisateurs ! Super, j'en suis très satisfait. Le problème est que l'approche la plus logique est de ne pas leur permettre de travailler sur cet instantané. Le script que j'ai écrit supprimera cette table à chaque actualisation et générera une nouvelle table identique portant le même nom. Je souhaite conserver cette configuration : cela empêchera les utilisateurs d'avoir 30 copies de la table maître à différents moments de l'instantané, sous des noms différents. L'étape suivante consiste à mettre à jour la table de données de travail de l'utilisateur avec les nouvelles données de la copie maître.
J'essaie d'écrire un script pour y parvenir. Il me faut mettre à jour la table de travail tout en préservant les ajouts des utilisateurs.
J'ai essayé d'utiliser la fonction Update(), mais d'après ce que j'ai compris, elle ne fait que remplacer les données existantes par de nouvelles. Je n'arrive pas à trouver comment ajouter de nouvelles lignes. Joindre ajoute de nouvelles lignes, mais crée également une nouvelle table. Aucune de ces solutions ne fonctionne pour moi. La seule solution que je trouve est la concaténation. Je ne veux pas que les utilisateurs aient à gérer des doublons. Ces doublons peuvent également être très graves : les tables de données contiennent des centaines de milliers de lignes, et il peut y en avoir jusqu'à 50 000 à la fois.
Mon approche actuelle (JSL inclus) consiste à créer des colonnes temporaires dans la table principale et la table de travail. Celles-ci concaténent l'ID d'échantillon || l'ID de cycle, créant ainsi une clé de colonne unique, appelée keyKey. Je souhaite ensuite créer une seconde colonne dans la table principale. Cette colonne comparera les keyKey des tables 1 et 2 ; appelons-la la colonne Selector. Si keyKey existe dans les deux tables, la valeur de la colonne Selector doit être 0 ; si elle n'existe que dans la table principale, elle doit être 1. Je souhaite ensuite que le script sélectionne toutes les lignes ayant une valeur de 1, crée une table de sous-ensemble temporaire basée sur cette sélection, puis concatène ce sous-ensemble à la table de travail. Boum ! Concaténation sans doublons. MAIS… Je n'arrive absolument pas à comparer les keyKey de deux tables et à utiliser cette comparaison pour générer une valeur de colonne.
J'essaie désespérément d'éviter d'utiliser des boucles For Each Row(). Je ne veux pas surcharger la mémoire des tables de données de plus de 100 000 lignes.
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." );
De plus, si quelqu'un pouvait m'aider à comprendre comment utiliser des variables de chaîne pour l'identification du nom de colonne (étape 1, commentée car je ne savais pas non plus comment résoudre ce problème), ce serait formidable.
Quoi qu'il en soit, merci d'avoir lu ce post colossal,
En retard
Automatisation et scriptsAccès aux donnéesPartage et communication des résultatsRé : Concaténation efficace sans doublons à l'aide de scripts en faisant correspondre les données de deux colonnes dans des tables différentesPourriez-vous utiliser Rejoindre -> créer une nouvelle table -> écraser l'ancienne table avec la table jointe nouvellement créée si vous souhaitez conserver la même table d'origine ?
Ré : Concaténation efficace sans doublons à l'aide de scripts en faisant correspondre les données de deux colonnes dans des tables différentesSi vous ne pouvez pas suivre la suggestion de Jarmo, vous pouvez toujours concaténer puis supprimer les doublons.
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()
);
Ré : Concaténation efficace sans doublons à l'aide de scripts en faisant correspondre les données de deux colonnes dans des tables différentes@jthi Je souhaite éviter de supprimer la table de travail de l'utilisateur. La concaténation est donc la seule option viable, si je comprends bien.
@mmarchandFSLR Je suppose que cela ressemble de plus en plus à la réponse la plus simple, même si j'hésite quant au risque de ralentissement dû à la multitude de doublons. Je suis simplement choqué qu'il semble impossible dans JMP de concaténer des données non dupliquées ; l'absence d'une fonction semble tellement importante…
Ré : Concaténation efficace sans doublons à l'aide de scripts en faisant correspondre les données de deux colonnes dans des tables différentesC'est pourquoi j'ai suggéré de faire une jointure puis d'utiliser cette table jointe pour « mettre à jour » la table d'origine.
JMP ne dispose pas actuellement de la fonctionnalité d'upsert (mise à jour et insertion) appropriée, mais vous pouvez créer la vôtre. Il peut toutefois être un peu compliqué d'en créer une efficace… Avez-vous également essayé si vous rencontrez des problèmes avec la sélection en double ? Ou s'agirait-il simplement d' une optimisation prématurée pour tenter de l'éviter ?
Ré : Concaténation efficace sans doublons à l'aide de scripts en faisant correspondre les données de deux colonnes dans des tables différentesNon, tu as raison. C'était une optimisation prématurée, et oui, la concaténation et la suppression des doublons fonctionnent avec notre ensemble de données, même si elles créent plusieurs centaines de milliers de doublons à supprimer. Cela me semble tout simplement une façon peu judicieuse d'obtenir ce qui, à mon avis, est une fonction assez basique.
Bien que je comprenne l'idée derrière l'approche de jointure, elle est sûrement toujours bloquée par Update() qui n'ajoute pas de nouvelles lignes ? À moins que je ne comprenne mal quelque chose, pourriez-vous clarifier ?
Code final épuré ci-dessous pour les curieux :
// ** 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;
Ré : Concaténation efficace sans doublons à l'aide de scripts en faisant correspondre les données de deux colonnes dans des tables différentesRien ne le bloque. Mon idée était d'utiliser la fonction « Joindre » pour créer une nouvelle table avec toutes les lignes, puis de l'utiliser pour écraser toutes les valeurs de l'ancienne. Cela peut être réalisé, par exemple, en utilisant l'abonnement aux tables de données.
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);
Vous pouvez également faire quelque chose comme cela, mais soyez prudent quant à la façon dont JMP peut éventuellement perturber les scripts/variables de table.
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);