Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@
^\.jules(/.*)?$
^\.trivyignore\.yaml$
^trivy\.yaml$
^\.semgrepignore$
^actionlint.*
^gitleaks.*
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@
## 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-08-03 - R 언어에서 데이터 프레임 2차원 할당을 1차원 벡터 할당으로 변경하여 병목 최적화
**Learning:** R에서 특정 조건에 맞는 데이터 프레임의 열 값을 업데이트할 때 2차원 서브셋팅 방식(`df[df$idx == 'val', 'col'] <- new_val`)을 사용하면 `[<-.data.frame` 메서드 디스패치 오버헤드와 함께 전체 차원 확인, 팩터 레벨 확인 및 깊은 복사가 발생하여 비효율적입니다.
**Action:** 데이터 프레임 내부의 특정 열 벡터에 직접 1차원 벡터 할당(`df$col[df$idx == 'val'] <- new_val`)을 수행하여 불필요한 O(N) 메서드 오버헤드를 건너뛰고 O(1) 수준의 직접 리스트 및 C-레벨 벡터 접근을 통해 성능을 비약적으로 높여야 합니다.
50 changes: 23 additions & 27 deletions R/aFIPC.R
Original file line number Diff line number Diff line change
Expand Up @@ -598,15 +598,15 @@ autoFIPC <-
# Preserve mirt's structural estimability flags. Forcing every row TRUE
# frees boundary parameters such as 2PL g/u and makes the Hessian unstable.

NewScaleParms[NewScaleParms$item == 'GROUP', "est"] <- FALSE
OldScaleParms[OldScaleParms$item == 'GROUP', "est"] <- FALSE
NewScaleParms$est[NewScaleParms$item == 'GROUP'] <- FALSE
OldScaleParms$est[OldScaleParms$item == 'GROUP'] <- FALSE

NewScaleParms[NewScaleParms$name == "COV_11", "est"] <- TRUE
OldScaleParms[OldScaleParms$name == "COV_11", "est"] <- TRUE
NewScaleParms$est[NewScaleParms$name == "COV_11"] <- TRUE
OldScaleParms$est[OldScaleParms$name == "COV_11"] <- TRUE

if (itemtype == 'Rasch') {
NewScaleParms[NewScaleParms$name == "a1", "est"] <- FALSE
OldScaleParms[OldScaleParms$name == "a1", "est"] <- FALSE
NewScaleParms$est[NewScaleParms$name == "a1"] <- FALSE
OldScaleParms$est[OldScaleParms$name == "a1"] <- FALSE
}

#IPD
Expand Down Expand Up @@ -785,16 +785,14 @@ autoFIPC <-
newIdx <- newScaleParmsItemIdxCache[[newFormItemStr]]
oldIdx <- oldScaleParmsItemIdxCache[[oldFormItemStr]]

# ⚡ Bolt: Remove unnecessary paste0() array string generation overhead
message(' Newform Parms: ', paste(NewScaleParms[newIdx, "value"], collapse = ' '))
message(' Oldform Parms: ', paste(OldScaleParms[oldIdx, "value"], collapse = ' '))
# ⚡ Bolt: Use 1D vector assignment instead of 2D dataframe assignment for better performance
message(' Newform Parms: ', paste(NewScaleParms$value[newIdx], collapse = ' '))
message(' Oldform Parms: ', paste(OldScaleParms$value[oldIdx], collapse = ' '))

NewScaleParms[newIdx, "value"] <-
OldScaleParms[oldIdx, "value"]
message(' Linkedform Parms: ', paste(NewScaleParms[newIdx, "value"], collapse = ' '), '\n')
NewScaleParms$value[newIdx] <- OldScaleParms$value[oldIdx]
message(' Linkedform Parms: ', paste(NewScaleParms$value[newIdx], collapse = ' '), '\n')

NewScaleParms[newIdx, "est"] <-
FALSE
NewScaleParms$est[newIdx] <- FALSE
} else {
message(
'skipping ',
Expand All @@ -813,17 +811,15 @@ autoFIPC <-
newBetaIdx <- NewScaleParms$item == 'BETA'
oldBetaIdx <- OldScaleParms$item == 'BETA'

NewScaleParms[newBetaIdx, "value"] <-
OldScaleParms[oldBetaIdx, "value"]
NewScaleParms[newBetaIdx, "est"] <-
FALSE
NewScaleParms$value[newBetaIdx] <- OldScaleParms$value[oldBetaIdx]
NewScaleParms$est[newBetaIdx] <- FALSE

message('applying BETA parameter as linking')

message(
' Linkedform Parms: ',
paste0(
NewScaleParms[newBetaIdx, "value"],
NewScaleParms$value[newBetaIdx],
Comment on lines +814 to +822

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

BETA 값 복사 전에 길이 일치를 확인하십시오.

newBetaIdxoldBetaIdx는 서로 다른 행 수를 가진 두 테이블의 논리 인덱스입니다. 두 모델의 BETA 파라미터 개수가 다르면 NewScaleParms$value[newBetaIdx] <- OldScaleParms$value[oldBetaIdx]가 값을 재활용하거나 오류를 냅니다. 1차원 할당은 2D 할당보다 재활용 경고가 약합니다. 따라서 잘못된 링킹 값이 조용히 들어갈 수 있습니다.

복사 전에 sum(newBetaIdx) == sum(oldBetaIdx)를 확인하고, 불일치 시 명확한 오류를 발생시키십시오.

🐛 제안 수정
       newBetaIdx <- NewScaleParms$item == 'BETA'
       oldBetaIdx <- OldScaleParms$item == 'BETA'
 
+      if (sum(newBetaIdx) != sum(oldBetaIdx)) {
+        stop('BETA parameter counts differ between forms; cannot link BETA parameters.')
+      }
+
       NewScaleParms$value[newBetaIdx] <- OldScaleParms$value[oldBetaIdx]
       NewScaleParms$est[newBetaIdx] <- FALSE
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@R/aFIPC.R` around lines 814 - 822, Before the BETA assignment in the linking
flow, validate that sum(newBetaIdx) equals sum(oldBetaIdx); if the counts
differ, stop with a clear error instead of performing the assignment. Keep the
existing NewScaleParms$value and OldScaleParms$value copy unchanged when the
lengths match.

' '
),
'\n'
Expand Down Expand Up @@ -858,13 +854,13 @@ autoFIPC <-
new_mean11_idx <- NewScaleParms$name == "MEAN_11"
old_mean11_idx <- OldScaleParms$name == "MEAN_11"

NewScaleParms[new_cov11_idx, "est"] <- FALSE
OldScaleParms[old_cov11_idx, "est"] <- FALSE
NewScaleParms[new_mean11_idx, "est"] <- FALSE
OldScaleParms[old_mean11_idx, "est"] <- FALSE
NewScaleParms$est[new_cov11_idx] <- FALSE
OldScaleParms$est[old_cov11_idx] <- FALSE
NewScaleParms$est[new_mean11_idx] <- FALSE
OldScaleParms$est[old_mean11_idx] <- FALSE

NewScaleParms[new_cov11_idx, "value"] <- 1
OldScaleParms[old_mean11_idx, "value"] <- 0
NewScaleParms$value[new_cov11_idx] <- 1
OldScaleParms$value[old_mean11_idx] <- 0
}
if (freeMEAN == T) {
LinkedModelSyntax <-
Expand All @@ -875,8 +871,8 @@ autoFIPC <-
'MEAN = F1'
))

NewScaleParms[NewScaleParms$name == "MEAN_1", "est"] <- TRUE
OldScaleParms[OldScaleParms$name == "MEAN_1", "est"] <- TRUE
NewScaleParms$est[NewScaleParms$name == "MEAN_1"] <- TRUE
OldScaleParms$est[OldScaleParms$name == "MEAN_1"] <- TRUE
} else {
LinkedModelSyntax <-
mirt::mirt.model(paste0(
Expand Down
18 changes: 18 additions & 0 deletions aFIPC.Rcheck/00_pkg_src/aFIPC/DESCRIPTION
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Package: aFIPC
Type: Package
Title: Automated Fixed Item Parameter Linking
Version: 0.1.0
Author: Seongho Bae [aut, cre]
Maintainer: Seongho Bae <seongho@kw.ac.kr>
Authors@R: person(given = "Seongho", family = "Bae", role = c("aut", "cre"),
email = "seongho@kw.ac.kr")
Description: Automates fixed item parameter linking for test linking under
the item response theory paradigm using mirt package estimates.
License: GPL-3 | file LICENSE
Imports: mirt, methods
Suggests: testthat (>= 3.0.0)
Encoding: UTF-8
Config/testthat/edition: 3
Config/roxygen2/version: 8.0.0
NeedsCompilation: no
Packaged: 2026-08-03 16:55:05 UTC; jules

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

aFIPC.Rcheck 생성 산출물을 커밋하지 마십시오.

이 파일의 Packaged 필드와 동일한 트리의 R/*.rdb, R/*.rdx, help/*.rdsR CMD check가 생성한 결과물입니다. 이 트리를 보존하면 생성 파일이 루트 R/ 소스와 달라질 수 있고, 소스 패키지의 빌드 입력과 크기를 오염시킬 수 있습니다. aFIPC.Rcheck/ 전체를 제거하고 .gitignore.Rbuildignore에 추가하십시오.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@aFIPC.Rcheck/00_pkg_src/aFIPC/DESCRIPTION` at line 18, Remove the generated
aFIPC.Rcheck/ tree, including its Packaged metadata and R/*.rdb, R/*.rdx, and
help/*.rds artifacts, from the changeset. Add aFIPC.Rcheck/ to both .gitignore
and .Rbuildignore so future R CMD check output is not tracked or included in
package builds.

2 changes: 2 additions & 0 deletions aFIPC.Rcheck/00_pkg_src/aFIPC/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
YEAR: 2026
COPYRIGHT HOLDER: Seongho Bae
5 changes: 5 additions & 0 deletions aFIPC.Rcheck/00_pkg_src/aFIPC/NAMESPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Generated by roxygen2: do not edit by hand

export(autoFIPC)
export(surveyFA)
import(mirt)
Loading
Loading