使用腳本透過匹配不同表中兩列的資料實現無重複的有效連接大家好,
距離上次發文已經有一段時間了,這段時間裡,我覺得我的 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);