使用脚本通过匹配不同表中两列的数据实现无重复的有效连接大家好,
距离上次发帖已经有一段时间了,这段时间里,我觉得我的 JMP 使用能力提升了一些。坦白说,利用蓬勃发展的 AI-LLM 自学 JSL 确实很有效。然而,我遇到了一个困扰我很久的 JMP 使用问题。
正如标题所述,它源于大型表格的连接,且不包含重复值。我使用的是 JMP 18 版本。我附加了两个测试数据表(显然数据集非常有限且经过了清理,实际数据集包含超过 10 万行,超过 30 列)。这些应该附加到项目中,哎呀。
让我解释一下背景(很多背景信息最终都指向一个非常直接的问题,抱歉):
我有一个合理开发的常规数据分析工作流程。我有一个样本跟踪表,其中包含样本 ID 及其相关信息。我还有一个通用数据表,我会用我的测试信息填充它。这两个表使用虚拟连接进行关联,以样本 ID 为键。在样本信息表中,每个样本 ID 都是唯一的。在周期数据表中,每个样本 ID 包含多行数据,每行(理想情况下)都有一个唯一的周期 ID。样本 ID 和周期 ID 的每个唯一组合都包含相关数据(各种性能指标)。
一个测试有多个循环——实际上,一个测试处于实时状态,其循环编号及其相关数据会不断更新。我会定期导出每个样本的循环数据,将其导入 JMP,然后连接两个表——最新快照和批量数据表。这个过程会产生很多重复数据。例如,以样本 A 为例。批量数据表中 A 的循环 ID 已经是 1 -> 1000。在最新快照中,A 将包含循环 ID 1 -> 1200 的数据。我连接这些表,批量数据表中将包含样本 A 的循环 ID 1 -> 1000 *2 和 1001 -> 1200 *1。
就我个人的工作流程而言,这并不理想,但还好。我选择所有列,选择所有重复的行,然后删除它们。虽然不是最优方案,但没关系。
这个问题实际上源于我最近的工作。我构建批量数据的方式以及与虚拟连接的结合是一种非常有效的数据过滤策略。我的团队注意到了这一点,并且我通过一些努力说服大家采用类似的策略。然而,我是我们团队中 JMP 水平最高的用户。其他人没有我那么多的时间或精力去学习处理 JMP/JSL 的复杂问题。这没关系,我乐于承担这份责任。
但是,我希望他们能够访问我处理数据的能力。
我想到了一个解决方案。我的想法是,将批量数据表维护为外部表。我将使用类似管理员的项目来更新它。我编写了一些脚本,可以在他们的项目中自动初始化,并定义一些函数来自动将主文件的本地嵌入副本刷新到他们的项目中。这些脚本运行良好——我可以成功调用其他人项目中主数据表的快照!太好了,我对此非常满意。问题在于,最合理的方法是实际上不允许他们处理该快照。我编写的脚本会在每次刷新时删除该表,并在其位置生成一个名称相同的新表。我希望保持这种情况——这样可以防止用户在不同的快照时刻拥有 30 个名称不同的主表副本。下一步是用主副本中的新数据更新用户自己的工作数据表。
我正在尝试编写一个脚本来实现这一点。我需要它更新工作表,同时保留用户自己添加的内容。
我试过使用 Update () 函数,但据我所知,它只会用新数据替换现有数据。我怎么也想不出怎么让它添加新行。Join 函数会添加新行,但也会创建一个新表。这两种方法对我来说都行不通。我能想到的唯一方法是连接。但我不想让用户处理重复的行。重复的行也可能非常严重——数据表有数十万行,一次可能有多达 50,000 个重复项。
我现在的方法(包括 JSL)是在主表和工作表中创建临时列。这些列连接样本 ID || 周期 ID,创建一个唯一的单列键,我们称之为 keyKey。然后,我想在主表中创建第二列。我希望此列比较表 1 和表 2 中的 keyKey,我们称之为 Selector 列。如果 keyKey 存在于两个表中,我希望 Selector 列中的值为 0;如果它仅存在于主表中,我希望该值为 1。然后,我希望脚本选择值为 1 的所有行,基于该选择创建一个临时子集表,然后将该子集连接到工作表。Boom。无重复连接。但是。我终其一生都无法弄清楚如何在两个表之间比较 keyKey 并使用该比较来生成列值。
我拼命避免使用 For Each Row() 循环。我不希望超过 100,000 行的数据表占用太多内存。
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." );
此外,如果有人能帮助我弄清楚如何使用字符串变量来识别列名(步骤 1,已注释掉,因为我也不知道如何解决这个问题),那就太好了。
无论如何,感谢你阅读这篇巨著,
晚的
自动化和脚本数据访问分享和交流结果回复:通过匹配不同表中两列的数据,使用脚本实现无重复的有效连接如果您希望保持原始表不变,可以使用连接->创建新表->用新创建的连接表覆盖旧表吗?
回复:通过匹配不同表中两列的数据,使用脚本实现无重复的有效连接如果您不能遵循 Jarmo 的建议,您可以始终连接然后删除重复项。
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()
);
回复:通过匹配不同表中两列的数据,使用脚本实现无重复的有效连接@jthi我想避免删除用户的工作表。因此,据我目前了解,连接是唯一可行的选择。
@mmarchandFSLR我觉得这越来越像是最简单的答案了,尽管我有点担心大量重复数据会拖慢速度。我只是很震惊,JMP 竟然无法连接非重复数据,感觉这么重要的功能竟然缺失了……
回复:通过匹配不同表中两列的数据,使用脚本实现无重复的有效连接这就是为什么我建议进行连接,然后使用连接表来“更新”原始表。
JMP 目前没有合适的 upsert(更新 + 插入)功能,但您可以自己编写。不过,创建高效的功能可能有点复杂……另外,您是否尝试过重复选择时遇到的问题?或者这只是为了避免这种情况而进行的过早优化?
回复:通过匹配不同表中两列的数据,使用脚本实现无重复的有效连接不,你说得对。这是过早的优化,而且合并和删除重复项确实适用于我们的数据集,尽管它创建了几十万个需要删除的重复项。在我看来,用这种方式实现一个相当基本的功能,看起来太丑了。
虽然我理解连接方法背后的原理,但它肯定还是会被 Update() 阻塞,无法添加新行吧?除非我误解了什么,否则你能解释一下吗?
对于那些好奇的人来说,下面是最终的清理代码:
// ** 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;
回复:通过匹配不同表中两列的数据,使用脚本实现无重复的有效连接它没有被任何东西阻塞。我的想法是使用 Join 创建包含所有行的新表,然后用它覆盖旧表中的所有值。例如,可以使用数据表下标来实现。
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);
您也可以做类似的事情,但要小心 JMP 可能会干扰表脚本/变量
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);