diff --git a/.github/workflows/r.yml b/.github/workflows/r.yml index cf2e656..004c0ed 100644 --- a/.github/workflows/r.yml +++ b/.github/workflows/r.yml @@ -29,6 +29,14 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - name: Install system dependencies + run: | + sudo mkdir -p /etc/needrestart/conf.d/ + echo "\$nrconf{restart} = 'a';" | sudo tee /etc/needrestart/conf.d/restart.conf > /dev/null + sudo apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ + libharfbuzz-dev libfribidi-dev libfreetype6-dev libpng-dev libtiff5-dev libjpeg-dev + - name: Set up R uses: r-lib/actions/setup-r@d3c5be51b12e724e68f33216ca3c148b66d5f0b6 with: @@ -38,7 +46,10 @@ jobs: - name: Set up R package dependencies uses: r-lib/actions/setup-r-dependencies@d3c5be51b12e724e68f33216ca3c148b66d5f0b6 with: - extra-packages: any::rcmdcheck + # Native package caches can outlive the runner's TBB ABI and make + # qs2 fail to load while SimDesign is installed. + cache: false + extra-packages: any::rcmdcheck url::https://cran.r-project.org/src/contrib/Archive/Deriv/Deriv_4.1.3.tar.gz needs: check - name: Run R CMD check diff --git a/.jules/bolt.md b/.jules/bolt.md index 7d3c603..dcd6e75 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -16,3 +16,7 @@ ## 2025-02-12 - R 언어에서 반복적인 mirt 모델 생성 시 불필요한 데이터프레임 부분집합 추출 최적화 **Learning:** R에서 데이터프레임의 특정 열을 추출하는 작업(`df[cols]`)은 O(N)의 메모리 복사를 수반합니다. `autoFIPC`에서 `mirt` 모델의 파라미터를 설정하거나 호출하는 과정 중에 `newformXDataK[colnames(newFormModel@Data$data)]` 코드가 반복해서 사용되었고, 심지어 `ncol()`을 위해 단순히 개수를 구할 때도 사용되어 불필요한 메모리 할당과 오버헤드를 초래했습니다. **Action:** 조건문이나 반복문 내부에서 불필요하게 데이터프레임 부분집합 연산이 반복되지 않도록 외부에서 한 번만 `linkedFormData <- newformXDataK[colnames(newFormModel@Data$data)]`로 캐싱(caching)한 뒤, `ncol(linkedFormData)`와 `data = linkedFormData` 형태로 재사용하여 메모리 복사와 O(N) 오버헤드를 방지해야 합니다. +## 2024-07-20 - R 언어에서 factor 생성 및 데이터 프레임 재구성 줄이기 + +**Learning:** `data.frame(df, col)`은 기존 프레임을 새 프레임으로 명시적으로 재구성하고, `as.factor(...)`는 레벨을 입력에서 추론합니다. 직접 열을 할당하고 고정 레벨을 선언하면 이 두 작업을 피할 수 있습니다. 다만 R의 copy-on-modify 의미론에 따라 열 할당도 복사를 유발할 수 있으므로 O(1)이나 고정 속도 향상을 보장하지 않습니다. +**Action:** 결과 의미론이 고정된 경우에만 `factor(..., levels = ...)`와 직접 열 할당을 사용하고, 구체적인 성능 수치는 대표 입력을 사용한 재현 가능한 벤치마크가 있을 때만 기록하십시오. diff --git a/R/aFIPC.R b/R/aFIPC.R index 6254651..825c65e 100644 --- a/R/aFIPC.R +++ b/R/aFIPC.R @@ -1,3 +1,11 @@ +.build_ipd_group <- function(old_count, new_count) { + factor( + rep(c('oldForm', 'newForm'), c(old_count, new_count)), + levels = c('newForm', 'oldForm') + ) +} + + #' automated fixed item parameter linking #' #' @import mirt @@ -612,11 +620,10 @@ autoFIPC <- #IPD if (checkIPD == T) { # config - IPDgroup <- - as.factor(c( - rep('oldForm', nrow(oldformYDataK)), - rep('newForm', nrow(newformXDataK)) - )) + IPDgroup <- .build_ipd_group( + nrow(oldformYDataK), + nrow(newformXDataK) + ) IPDItemCount <- 0 # IPD target item checking @@ -1044,7 +1051,9 @@ autoFIPC <- modelReturn$ThetaNewform <- ThetaNewform modelReturn$ThetaLinkedform <- ThetaLinkedform if (checkIPD) { - modelReturn$IPDData <- data.frame(IPDData, IPDgroup) + # ⚡ Bolt: Append column using direct list assignment instead of O(N) data.frame() concatenation + modelReturn$IPDData <- IPDData + modelReturn$IPDData$IPDgroup <- IPDgroup if (exists('CommonItemList_NOIPD', inherits = FALSE)) { modelReturn$IPDCommonItemList <- IPDItemList[CommonItemList_NOIPD] } diff --git a/tests/testthat/test-autoFIPC.R b/tests/testthat/test-autoFIPC.R index 13cecd9..b0beef2 100644 --- a/tests/testthat/test-autoFIPC.R +++ b/tests/testthat/test-autoFIPC.R @@ -89,3 +89,38 @@ test_that("autoFIPC validates input types securely", { "Security Error: tryEM must be a single non-NA logical value" ) }) + +test_that("autoFIPC factors missing common items", { + expect_error( + aFIPC::autoFIPC( + newformXData = data.frame(A=1), + oldformYData = data.frame(A=2), + newformCommonItemNames = c('A', 'B'), + oldformCommonItemNames = c('A') + ), + "Common Items are not equal" + ) +}) + +test_that("autoFIPC catches 0 length common items", { + expect_error( + aFIPC::autoFIPC( + newformXData = data.frame(A=1), + oldformYData = data.frame(A=2), + newformCommonItemNames = character(0), + oldformCommonItemNames = character(0) + ), + "Please provide common item names" + ) +}) + + +test_that("IPD groups preserve the new form as mirt reference level", { + groups <- aFIPC:::.build_ipd_group(old_count = 2L, new_count = 3L) + + expect_identical(levels(groups), c("newForm", "oldForm")) + expect_identical( + as.character(groups), + c("oldForm", "oldForm", "newForm", "newForm", "newForm") + ) +})