卡方檢驗值轉換為P值
卡方檢驗作為一種常見的假設檢驗,在統計學中的地位是顯而易見的,如果你還不太清楚可以參看這篇博文:卡方檢驗用於特徵選擇,寫的非常的淺顯易懂,如果你還想再擴充套件點卡方檢驗方面的知識,可以參看這篇博文卡方檢驗基礎,寫的也很有意思。前輩的功底都很深厚,小弟就就不再闡述卡方檢驗的原理、意義及如何計算了,理解了其實很簡單就那麼個公式,再根據實際業務場景關鍵看你選擇哪一個。從chi-squared value 到p-value,相信大多數同學和我一樣,查表,因為大學課本上就是這麼寫的。假如在實際業務場景中,自由度和顯著性水準都不確定的情況下,怎麼辦呢?查表就顯得不那麼地道了。
這時可能很多同學想到了著名的fisher精確檢驗,因為這個檢驗能直接求出的精確的p-value,但是在檢驗資料樣本比較大的情況下,fisher精確檢驗的計算複雜度會讓你顯得那麼的力不從心,本系列的後面將會講到fisher精確檢驗的原理並給出其原始碼及與chi-squared的效率對比。還是抓緊時間侃侃怎麼通過chi-squared計算p-value吧,估計心急的同學就等不及了。ok,咱們攻城師還是用程式碼說話,先上程式碼:
public double chisqr2pValue(int dof, double chi_squared) { if (chi_squared < 0 || dof < 1) { return 0.0; } double k = ((double) dof) * 0.5; double v = chi_squared * 0.5; if (dof == 2) { return Math.exp(-1.0 * v); } double incompleteGamma = IncompleteGamma.log_igf(k,v); // 如果過小或者非數值或者無窮 if (Math.exp(incompleteGamma) <= 1e-8 || Double.isNaN(Math.exp(incompleteGamma)) || Double.isInfinite(Math.exp(incompleteGamma))) { return 1e-14; } double gamma = Math.log(Gamma.getApproxGamma(k)); incompleteGamma -= gamma; if(Math.exp(incompleteGamma) > 1){ return 1e-14; } double pValue = 1.0 - Math.exp(incompleteGamma); return (double) pValue; }
這個chisqr2pValue函式引用到了兩個函式,一個為getApproxGamma(k) 一個為log_igf(k,v),如果你對該函式的實現原理不太清楚,wiki中侃的很清楚:卡方分佈,本文也不再闡述卡方分佈的密度函式、自由度等等;具體求P-value說白了就是計算卡方分佈的分佈函式值,如下的公式:
其中,分子為不完全伽馬函式,分母為伽馬函式;那麼上述chisqu2pvalue就是實現了上述公式。Ok,現在的問題轉換為怎麼求分子與分母。
在兩年前我曾為了實現伽馬函式的功能程式碼敲個積分,但是效果不太理想。但,今天發現個利器:斯特靈公式,估計大多數同學跟我一樣,第一感覺斯特靈公式不是用來求階乘的嗎?不錯,確實是,在實現fisher時我也用到了斯特靈公式;可是哥們確實有點孤陋寡聞,斯特靈還有個求伽馬函式的近似公式,如下:
如果你想了解詳細的斯特靈公式的推導資訊,也是參考wiki:斯特林公式 是不是覺得維基太牛逼了?好吧,還是趕緊上程式碼:
public static double getApproxGamma(double n) {
// RECIP_E = (E^-1) = (1.0 / E)
double RECIP_E = 0.36787944117144232159552377016147;
// TWOPI = 2.0 * PI
double TWOPI = 6.283185307179586476925286766559;
double d = 1.0 / (10.0 * n);
d = 1.0 / ((12* n) - d);
d = (d + n) *RECIP_E;
d = Math.pow(d,n);
d *= Math.sqrt(TWOPI/ n);
return d;
}
好,剩下的就差不完全伽馬函式沒有解了,怎麼求?問大神wiki:不完全伽馬函式 哎呦,在裡面又發現個牛逼的不用計算積分的公式:
而M函式為什麼呢?就是傳聞中的合連幾何函式,如果對這個函式感興趣的可以繼續參考wiki:合連幾何函式 ,不過這裡我們依舊只使用它的一個公式:
Ok,公式知道了,程式碼實現就很簡單了,只不過在這裡為了便於大數的計算,我採用了計算其log值,程式碼如下:
public static double log_igf(double s, double z) {
if (z < 0.0) {
return 0.0;
}
double sc = (Math.log(z) * s) - z - Math.log(s);
double k = KM(s, z);
return Math.log(k) + sc;
}
private static double KM(double s, double z) {
double sum = 1.0;
double nom = 1.0;
double denom = 1.0;
double log_nom = Math.log(nom);
double log_denom = Math.log(denom);
double log_s = Math.log(s);
double log_z = Math.log(z);
for (int i = 0; i < 1000; ++i) {
log_nom += log_z;
s++;
log_s = Math.log(s);
log_denom += log_s;
double log_sum = log_nom - log_denom;
sum += Math.exp(log_sum);
}
return sum;
}
Ok,簡單吧?!以後你再也不用坑爹地查表了。下一篇再介紹下fisher精確檢驗吧。