서로 다른 테이블의 두 열에 있는 데이터를 일치시켜 스크립팅을 사용하여 중복 없이 효과적인 연결을 수행합니다.안녕하세요 여러분,
지난번 글을 올린 지 꽤 시간이 흘렀는데, 그동안 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은 새 행을 추가하지만 새 테이블도 생성합니다. 두 방법 모두 효과가 없습니다. 제가 알아낸 유일한 방법은 연결하는 것입니다. 하지만 사용자가 중복 행을 처리하도록 하고 싶지 않습니다. 중복은 매우 심각할 수 있습니다. 데이터 테이블은 수십만 개의 행으로 이루어져 있고, 한 번에 최대 5만 개의 중복 행이 발생할 수 있습니다.
지금 제가 사용하는 방식(JSL 포함)은 마스터 테이블과 작업 테이블에 임시 열을 만드는 것입니다. 이 열은 샘플 ID || 사이클 ID를 연결하여 고유한 단일 열 키를 만들고 이를 keyKey라고 부르겠습니다. 그런 다음 마스터 테이블에 두 번째 열을 만들고 싶습니다. 이 열이 테이블 1과 테이블 2의 keyKey를 비교하도록 하고 싶습니다. 이를 Selector 열이라고 부르겠습니다. keyKey가 두 테이블에 모두 있는 경우 Selector 열의 값을 0으로 하고, 마스터에만 있는 경우 값을 1로 하고 싶습니다. 그런 다음 스크립트에서 값이 1인 모든 행을 선택하고 해당 선택을 기반으로 임시 하위 집합 테이블을 만든 다음 해당 하위 집합을 작업 테이블에 연결하도록 하고 싶습니다. 붐. 중복된 무료 연결입니다. 하지만 두 테이블 간의 keyKey를 비교하고 해당 비교를 사용하여 열 값을 생성하는 방법을 알아낼 수가 없습니다.
For Each Row() 루프 사용을 최대한 피하려고 합니다. 행 크기가 10만 개가 넘는 데이터 테이블에 메모리 부하가 발생하는 것을 원치 않습니다.
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단계, 수정 방법을 알아낼 수 없어서 주석으로 처리했습니다).
어쨌든, 이 엄청난 글을 읽어주셔서 감사합니다.
늦은
자동화 및 스크립팅데이터 액세스결과 공유 및 전달Re: 서로 다른 테이블의 두 열에 있는 데이터를 일치시켜 스크립팅을 사용하여 중복 없이 효과적인 연결원본 테이블을 동일하게 유지하려면 조인 -> 새 테이블 만들기 -> 새로 만든 조인된 테이블로 기존 테이블 덮어쓰기를 사용할 수 있나요?
Re: 서로 다른 테이블의 두 열에 있는 데이터를 일치시켜 스크립팅을 사용하여 중복 없이 효과적인 연결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()
);
Re: 서로 다른 테이블의 두 열에 있는 데이터를 일치시켜 스크립팅을 사용하여 중복 없이 효과적인 연결@jthi 사용자 작업 테이블이 삭제되는 것을 방지하고 싶습니다. 따라서 제가 아는 한 연결(concatenation)이 유일하게 가능한 선택입니다.
@mmarchandFSLR 중복 데이터가 너무 많아서 속도가 느려질 가능성은 좀 걱정되지만, 그래도 점점 더 간단한 답처럼 보이네요. JMP에서 중복되지 않은 데이터를 연결하는 기능이 불가능해 보이는 게 너무 충격적이에요. 너무 큰 기능이 없어서 그런 것 같아요...
Re: 서로 다른 테이블의 두 열에 있는 데이터를 일치시켜 스크립팅을 사용하여 중복 없이 효과적인 연결그래서 저는 조인을 한 다음 조인된 테이블을 사용하여 원래 테이블을 "업데이트"하는 것을 제안했습니다.
JMP에는 현재 제대로 된 upsert(update + insert) 기능이 없지만, 직접 작성할 수는 있습니다. 다만 효율적인 upsert를 만드는 건 약간 복잡할 수 있습니다. 그리고 중복 선택에 문제가 있다면 시도해 보셨나요? 아니면 문제를 피하기 위한 성급한 최적화 일까요?
Re: 서로 다른 테이블의 두 열에 있는 데이터를 일치시켜 스크립팅을 사용하여 중복 없이 효과적인 연결아뇨, 맞아요. 성급한 최적화였고, 네, 중복 연결 및 삭제 기능은 데이터 세트에서 작동합니다. 삭제해야 할 중복이 수십만 개나 생성된다는 사실에도 불구하고요. 제 생각에는 꽤 기본적인 기능을 구현하는 방식이 너무 안 좋은 것 같습니다.
조인 방식의 원리는 이해하지만, 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;
Re: 서로 다른 테이블의 두 열에 있는 데이터를 일치시켜 스크립팅을 사용하여 중복 없이 효과적인 연결아무것도 차단되지 않습니다. 제 아이디어는 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);