셀에 =NORM.DIST(115,100,15,TRUE)를 입력하면 Excel은 정상적으로 0.8413447을 반환합니다. 단순한 조견표 검색 같지만 실제로는 그렇지 않습니다. 이 수치의 배후에는 닫힌 형식(closed form)이 없는 적분인 누적 정규 분포가 작동하며, CHISQ.INV.RT와 BETA.DIST 등은 라이브러리가 단순 근사치가 아닌 엄밀하게 수치 연산해야 하는 특수 함수를 동반합니다. Excel 호환성을 충족하려는 스프레드시트 컴포넌트는 단순히 함수 이름을 제공하는 것을 넘어, 수치 연산 로직을 정밀하게 구현하여 Excel이 출력하는 마지막 자릿수까지 그대로 재현해야 합니다.
HotXLS는 50개 이상의 다양한 통계 함수를 지원하며, 수식 입력줄에서는 이면의 복잡한 수치 연산 프로세스가 전혀 보이지 않습니다. 이 글에서는 통계 엔진의 연산 구조를 분석합니다: 공통 특수 함수 코어, 수치적 안정성을 보장하기 위한 분기 제어, 그리고 일반적인 조건에서는 잘 드러나지 않아 오랫동안 미해결로 남아 있었던 역정규(inverse-normal) 분포 꼬리 영역의 로그 연산 버그를 알아봅니다.
하나의 워크시트 호출, 그 뒤에 배치된 50가지 분포
지원하는 함수군에는 다양한 통계적 분포가 포함됩니다. 정규 분포 계열인 NORM.DIST, NORM.S.DIST 및 해당 역함수들, 감마 및 카이제곱 분포 계열인 GAMMA.DIST, CHISQ.DIST, CHISQ.DIST.RT, CHISQ.INV.RT, 베타 분포 계열인 BETA.DIST 및 BETA.INV, 표본 분포 계열인 T.DIST, T.DIST.2T, F.DIST, F.INV, 이산형 분포인 BINOM.DIST, POISSON.DIST, 신뢰구간 도구인 CONFIDENCE.T, CONFIDENCE.NORM 등이 있습니다. 수식 호출 구조는 간단합니다. 셀에 변수를 입력하고 계산을 요청하여 결과를 읽어올 수 있습니다.
var
wb: IXLSWorkbook;
sh: IXLSWorksheet;
begin
wb := TXLSWorkbook.Create;
sh := wb.Sheets.Add;
sh.Range['A1', 'A1'].Value := 115; // observation
sh.Range['A2', 'A2'].Value := 100; // mean
sh.Range['A3', 'A3'].Value := 15; // standard deviation
// The XLS formula parser uses ';' as the argument separator.
Writeln(wb.Calculate('=NORM.DIST(A1;A2;A3;TRUE())')); // 0.8413447
Writeln(wb.Calculate('=CHISQ.INV.RT(0.05;10)')); // 18.3070381
Writeln(wb.Calculate('=BETA.DIST(0.5;2;3;TRUE())')); // 0.6875
end;
통합 문서의 Calculate 메서드는 즉석에서 수식을 컴파일하여 워크시트에서 계산을 진행하고 Variant 형식으로 결과를 반환합니다. 초기에 혼동하기 쉬운 한 가지 규칙은, Calculate의 수식 파서가 인수 구분자로 세미콜론(;)을 요구한다는 점입니다: 즉 =SUM(A1,B1)이 아니라 =SUM(A1;B1)로 작성해야 합니다. 셀에 직접 저장되는 수식은 표준 Excel 형태인 쉼표(,)를 유지합니다. 동일한 통계 연산 모듈이 모든 통계 함수를 처리하므로, Calculate 메서드를 통한 계산 경로는 모두 동일하게 매핑됩니다.
다른 모든 기능의 기반이 되는 두 가지 함수
여기에 나열된 대부분의 누적 분포 함수(CDF)는 공식 자체를 직접 적분하거나 적산하는 방식으로 연산하지 않습니다. 대신 정규화된 불완전 하부 감마 함수(regularized lower incomplete gamma)인 P(a, x)와 정규화된 불완전 베타 함수(regularized incomplete beta)인 Ix(a, b)라는 두 특수 함수를 기반으로 연산합니다. 이들은 내부적으로 연산 엔진을 지원하는 코어 모듈입니다. 카이제곱 CDF는 감마 CDF(모양 매개변수 df/2, 배율 2)와 동일합니다. 감마 CDF는 P(a, x) 자체입니다. t 분포, F 분포, 이항 누적 분포 역시 매개변수 값에 따른 정규화된 불완전 베타 함수의 출력값과 같습니다. 포아송 CDF는 불완전 상부 감마 함수 Q로 매핑됩니다. 즉, 이 두 개의 감마 및 베타 함수가 정밀하게 구성되면 수십 가지 분포 함수의 연산 정확도가 함께 상향 조정됩니다.
'정규화(regularized)' 처리가 핵심입니다. 일반적인 불완전 감마는 팩토리얼처럼 빠르게 증가하고 불완전 베타 적분은 연산 결과가 수렴하기도 전에 오버플로나 언더플로를 일으키기 쉽습니다. 정규화된 형식은 완전 감마 또는 완전 베타로 결과값을 나누어 0에서 1 사이의 범위에 안착시킵니다. 이는 확률이 가질 수 있는 정확한 범위와 일치합니다. 이러한 정규화 기술 덕분에 동일한 모듈이 자유도 2의 카이제곱 분포와 자유도 200의 카이제곱 분포를 중간 값의 오버플로 없이 double 정밀도로 매끄럽게 처리할 수 있습니다. 또한 누적 분포를 밀도 함수들의 긴 꼬리를 단순히 더하는 방식으로 계산해서는 안 되는 이유이기도 합니다: 각 연산 단위별로 오차가 누적되어 최종 합계에 반영되기 때문입니다. 정규화된 특수 함수는 복잡한 합산 대신 빠르게 수렴하는 급수나 연분수(continued fraction)를 직접 연산하여 오차 요인을 효과적으로 제거합니다.
대각선 아래의 급수, 대각선 위의 연분수
불완전 감마 루틴은 본격적인 계산 전에 조건 검사를 거칩니다: x 값을 a + 1과 비교하는 것입니다. 이 기준 경계는 통계적 근거를 가집니다. P(a, x)의 거듭제곱 급수(power-series) 전개식은 x 값이 a보다 상대적으로 작을 때 신속하게 수렴하지만, x가 크면 느리게 수렴하여 효용성이 떨어집니다. 연분수 공식은 이와 반대 특성을 보입니다. 따라서 엔진은 x가 a + 1보다 작을 때는 급수 공식을 적용하고, a + 1 이상일 때는 Lentz 연분수 공식을 적용하는 분기 처리를 통해 각 연산 구간에서 가장 안정적인 공식을 선택합니다.
연분수 공식은 한 가지 방어 코드가 필요합니다. Lentz의 해법은 분모와 분자를 연속적으로 유지하면서 매 단계 분모의 역수를 계산하는 구조인데, 만약 수치가 0에 가까워지면 역수 연산 시 에러가 발생합니다. 해결책은 미세한 하한선을 두는 것입니다: 중간 연산 도중 수치 크기가 1e-30 미만으로 떨어지면 이를 1e-30으로 고정(clamp)하여 수렴값에 영향을 주지 않으면서 재귀 계산의 연속성을 유지합니다. 불완전 베타의 연분수 구현에서도 동일한 하한선 보정이 적용됩니다. 아주 미세한 상수이지만 0으로 나누기 오류를 방지하고 연산 프로세스의 전체 수치 안정성을 담보하는 중요한 장치입니다.
상부 꼬리 확률인 Q(a, x)는 단순히 1에서 P(a, x)를 차감한 결과이며, 포아송 누적 분포 역시 이 원리로 계산됩니다: 즉 평균이 λ일 때 최대 k번의 이벤트가 발생할 확률은 Q(k + 1, λ)입니다. k + 1개의 포아송 밀도를 합산하는 것보다 상부 불완전 감마로 연산하는 것이 무수한 소수점 연산 누적을 방지하고 빠르게 수렴하게 만드는 효율적인 방식입니다.
팩토리얼 오버플로 없는 이산 확률 질량 구하기
이산 분포는 다른 형태의 위험 요소를 가집니다. 이항 확률 질량 함수는 이항계수(binomial coefficient) 연산이 필요하며, '52개 중 26개 선택'과 같은 계수 계산에는 매우 큰 수가 유입됩니다. 이를 직접 연산하면 나누기 연산 단계에 진입하기도 전에 double 분자 범위에서 오버플로를 일으키게 됩니다. 엔진은 수치 값을 직접 계산하지 않습니다. 대신 로그 감마 함수를 통해 로그 영역(log space)에서 팩토리얼을 계산한 후, 로그 값을 덧셈과 뺄셈으로 연산하고, 성공 및 실패 확률의 로그 값을 가산한 다음 최종 단계에서 한 번만 지수(Exp) 연산을 수행합니다.
// Binomial probability mass, evaluated entirely in log space.
// LnGammaF(n+1) is ln(n!); the three log-factorials form ln(C(n,k)),
// and the whole exponent is built before a single Exp call.
// ln P(X=k) = ln(n!) - ln(k!) - ln((n-k)!) + k*ln(p) + (n-k)*ln(1-p)
result := Exp(LnGammaF(nt + 1) - LnGammaF(kk + 1) - LnGammaF(nt - kk + 1)
+ kk * Ln(pp) + (nt - kk) * Ln(1 - pp));
로그 감마 함수 자체는 양의 영역 전체에서 연산 정밀도가 높은 Lanczos 근사식을 채택하고 있어 비용이 적게 듭니다. 모든 큰 값들은 마지막 Exp가 수행되기 전까지 로그 형태로 유지되므로, 연산 도중에 생성되는 가장 큰 수치는 최종 확률 값(최대 1)에 불과합니다. 포아송 질량 함수 역시 동일한 원리로 분모의 팩토리얼 연산을 단일 로그 감마 항으로 대체하여 연산합니다. p가 정확히 0이거나 1인 극단적인 경계값의 경우에는 별도의 예외 처리를 거치므로 Ln(0)이 실행되지 않습니다. HotXLS는 BINOM.DIST(5,10,0.5,FALSE)에 대해 0.2460938을 반환하고 누적 분포 POISSON.DIST(2,2,TRUE)에 대해 0.6766764를 반환하여 인쇄되는 자릿수까지 Excel과 완벽하게 부합합니다.
순방향 곡선 브래킷을 통한 역함수 구하기
역분포 함수는 정반대의 연산을 요구합니다: 즉 확률이 주어졌을 때 누적 분포 함수(CDF) 결과가 해당 확률에 수렴하는 값을 찾는 것입니다. 이 계열 중 오직 하나의 역함수만이 간단한 직접 수식을 지원합니다. 표준 역정규 분포 함수인 NORM.S.INV는 전체 영역에서 double 오차 범위의 높은 정확도를 내는 Acklam 유리 근사식(rational approximation)을 채택하고 있으며, 정밀 다항식 비율을 활용해 반복 연산(iteration) 없이 즉각 계산을 수행합니다.
그 외의 역함수들은 직접적인 수식이 없으므로 엔진이 수치 해석적으로 역연산을 구현합니다. 분포 영역의 유효 범위 내에서 상한과 하한 구간(bracket)을 정한 후 이진 분할(bisection)을 진행합니다: 중간값에서 순방향 CDF를 연산하고 목표 확률을 포함하도록 경계를 조정한 뒤 해당 범위가 충분히 좁혀질 때까지 반복합니다. 감마 및 카이제곱 역함수의 경우 0부터 모양 및 배율 변수를 기준으로 도출한 대략적인 상한선으로 계산을 시작하며, 목표 확률에 미치지 못하면 상한값을 두 배로 증폭시킵니다. t 분포 역함수는 양방향으로 확장되는 대칭 경계에서 작동하고, F 분포 역함수는 양의 구간 내에서 수렴 연산을 실행합니다. 호출당 수십 번의 CDF 연산이 발생하지만 스프레드시트 속도 기준으로는 무시할 수 있을 만큼 빠rmㅕ, 역함수의 정밀도가 순방향 통계 함수의 성능과 정확히 일치한다는 강점이 있습니다. 이를 통해 CHISQ.DIST(CHISQ.INV(0.7,5),5,TRUE)와 같은 교차 연산이 오차 없이 정확히 0.7을 반환할 수 있습니다.
꼬리 영역에 숨겨진 상용로그
오랫동안 발견되지 않았던 흥미로운 버그 사례가 있습니다. Acklam 역정규 분포 모듈은 세 분기 경로를 가집니다. 확률이 약 0.025와 0.975 사이일 때 실행되는 중앙 분기는 로그 함수가 개입되지 않는 다항식 비율을 통해 연산합니다. 반면 확률이 매우 작거나 매우 클 때 실행되는 두 개의 꼬리 영역 분기는 먼저 입력값의 로그 연산을 처리합니다. 꼬리 분포 곡선이 마이너스 자연로그(ln) 값의 제곱근 형태를 띠기 때문입니다.
과거 초기 구현 버전의 꼬리 영역 분기에서 자연로그가 쓰여야 할 위치에 실수로 상용로그(base-10 log)가 적용되어 있었습니다. 두 로그 함수의 결과는 약 2.30배 차이 나므로 꼬리 영역 연산 시 항상 일정한 오차가 누적되어 출력되었습니다. 하지만 대다수의 일반적인 검증 수준에서는 항상 중간값 위주로 확인을 거치기 때문에 이 버그가 잘 발견되지 않았습니다: 즉 NORM.S.INV(0.5) 결과가 0이고 교과서적인 기준값인 NORM.S.INV(0.975) 결과가 1.959964로 정확했던 것인데, 이 둘 모두 로그 연산이 전혀 없는 중앙 다항식에서 처리되었기 때문입니다. 오차는 확률 값이 꼬리 영역 깊숙이 이동하여 NORM.S.INV(0.001)(결과값 -3.0902323) 등이 호출될 때 비로소 노출되었습니다. 신뢰 구간 산출 도구를 포함해 꼬리 영역의 역정규 함수를 연동하는 모든 모듈이 이 오차의 영향을 받아왔습니다. 교훈은 명확합니다: 분기 구조를 가진 함수의 테스트 케이스는 반드시 각 분기 경로마다 골고루 배치되어야 하며, 다수의 정상적인 메인 경로가 일부의 버그 경로를 오랜 시간 마스킹할 수 있기 때문입니다. 단 한 줄의 로그 종류 수정을 통해 꼬리 영역 분포 값이 정상화되었습니다.
x의 부호가 t-분포의 꼬리 방향을 결정
Student t 누적 분포 함수에는 혼동하기 쉬운 중요한 특징이 있습니다. 해당 수치 값은 df / (df + x²) 영역에서 연산된 정규화된 불완전 베타 함수 값을 기준으로 하지만, 이 베타 값은 x 크기를 벗어난 꼬리 영역의 확률일 뿐 x까지의 누적 분포 확률 자체를 나타내지 않습니다. t 분포의 대칭 구조상 변환 식은 x의 부호 경계에 따라 대조적으로 구성되어야 합니다.
// Student t CDF. ib is the regularized incomplete beta at df/(df+x*x),
// which measures the symmetric tail. The cumulative value depends on
// the sign of x; returning ib unconverted gives the wrong tail.
ib := BetaIF(df / 2, 0.5, df / (df + x * x));
if x > 0 then
result := 1 - 0.5 * ib // above the mean: one minus half the tail
else if x < 0 then
result := 0.5 * ib // below the mean: half the tail
else
result := 0.5; // exactly at the mean
x가 0보다 큰 경우 누적 확률은 1에서 양방향 꼬리 확률의 반을 제한 값이며, x가 0보다 작은 경우에는 꼬리 확률의 반에 해당하고, 0인 경우에는 정확히 0.5가 됩니다. 만약 베타 값을 가공 없이 그대로 반환하면 임의의 nonzero x 값에 대해 곡선 전체의 방향이 달라져 잘못된 누적 분포가 출력됩니다. 우측 꼬리 및 양측 꼬리 분포 변형 함수들도 모두 이 분기 원리를 기반으로 연산하므로 T.DIST.2T(1,1)은 0.5를 반환하고 T.DIST(1,1,TRUE)는 0.75를 반환하며, 역함수인 T.INV는 이 보정된 CDF를 기준으로 이진 분할 연산을 처리하여 양방향 연산 호환성이 유지됩니다.
이 모든 과정은 셀 내부에서 감추어지며, 그것이 이 연산 모듈의 본래 목적입니다. 수식을 입력하면 Excel 결과와 완벽하게 일치하는 수치를 얻을 수 있습니다. 엔진에 독자적인 로직을 수식으로 등록하는 방법은 수식 엔진과 사용자 정의 함수 가이드에서 다루며, 다른 시트나 정의된 명명 영역을 교차 참조하는 수식 연산 방식은 정의된 이름과 시트 간 참조 수식 문서에 설명되어 있습니다. 이 모든 통계 분석 기능은 파일의 압축, 로드, 차트 생성 API와 함께 Delphi 및 C++Builder용 HotXLS 스프레드시트 컴포넌트에 내장되어 제공됩니다.