Module stikpetP.other.poho_friedman
Expand source code
import pandas as pd
from scipy.stats import rankdata
from statistics import NormalDist
from scipy.stats import t
from scipy.stats import studentized_range
def ph_friedman(data, levels=None, method = "dunn", ties=True):
'''
Post-Hoc Tests for a Friedman Test
----------------------------------
A post-hoc test after a Friedman test can be used to determine which variables differ significantly.
This function provides three options: Dunn, Conover, and Nemenyi.
Parameters
----------
data : dataframe
dataframe with a column for each variable
levels : dataframe or dictionary, optional
indication of what the levels are in order
method : {"dunn", "conover", "nemenyi"}, optional
Post-Hoc method. Default is "dunn"
ties : boolean, optional
apply a ties correction. Default is True
Returns
-------
res : dataframe
with the following columns
* *field 1*,
* *field 2*,
* *n*, sample size
* *statistic", test statistic used
* *df*, degrees of freedom (if applicable)
* *p-value*, the p-value (significance)
* *adj. p-value*, Bonferroni adjusted p-value
Notes
-----
**Conover**
Bartz-Beielstein et al. (2010, p. 319) attributes this to Conover (1999) (but also seen sites refering to Conover (1980), just different editions of the book) and uses as a formula:
$$t_{1,2} = \\frac{\\left|R_1 - R_2\\right|}{SE}$$
$$SE = \\sqrt{\\frac{2\\times n\\times\\left(1-\\frac{\\chi_F^2}{n\\times\\left(k-1\\right)}\\right)\\times\\left(\\sum_{i=1}^n\\sum_{j=1}^k r_{i,j}^2 - \\frac{n\\times k\\times\\left(k+1\\right)^2}{4}\\right)}{\\left(n-1\\right)\\times\\left(k-1\\right)}}$$
With:
$$R_j = \\sum_{i=1}^n r_{i,j}$$
In the original source it mentions \\(2\\times k\\) in \\(SE\\) instead of \\(2\\times n\\), this was indeed an error (pers. comm. with Conover).
Gnambs (n.d.) and BrightStat (n.d.) show a different formula, that gives the same result, and is the one the function uses:
$$SE = \\sqrt{\\frac{2\\times\\left(\\sum_{i=1}^n\\sum_{j=1}^k r_{i,j}^2 - \\sum_{j=1}^k R_j^2\\right)}{\\left(n-1\\right)\\times\\left(k-1\\right)}}$$
The significance is then determined using:
$$sig. = 2\\times\\left(1 - T\\left(\\left|t_{i,2}\\right|, df\\right)\\right)$$
Note that in the calculation \\(SE\\) is determined using all ranks, including those not in the comparison.
**Nemenyi**
Pohlert (2016, p. 15) shows the formula from Nemenyi (1963) as well as in Demšar (2006, pp. 11-12):
$$q_{1,2} = \\frac{\\left|R_1 - R_2\\right|}{\\sqrt{\\frac{k\\times\\left(k+1\\right)}{6\\times n}}}\\times\\sqrt{2}$$
$$df = n - k$$
This follows then a studentized range distribution with:
$$sig. = 1 - Q\\left(q_{1,2}, k, df\\right)$$
**Dunn**
Benavoli et. al (2016, pp. 2-3) and IBM SPSS (2021, p. 814):
$$z_{1,2} = \\frac{\\left|R_1 - R_2\\right|}{SE}$$
$$SE = \\sqrt{\\frac{k\\times\\left(k+1\\right)}{6\\times n}}$$
This follows a standard normal distribution:
$$sig. = 2\\times\\left(1 - \\Phi\\left(\\left|z_{i,2}\\right|\\right)\\right)$$
**Bonferroni adjustment**
The Bonferroni adjustment is done using:
$$sig._{adj} = \\min \\left(sig. \\times n_c, 1\\right)$$
$$n_c = \\frac{k\\times\\left(k-1\\right)}{2}$$
*Symbols Used*
* \\(n\\), the number of cases
* \\(k\\), the number of variables
* \\(r_{i,j}\\), the rank of case i, in variable j. The ranks are determined for each case.
* \\(\\Phi\\left(\\dots\\right)\\), the standard normal cumulative distribution function.
* \\(Q\\left(\\dots\\right)\\), the studentized range distribution cumulative distribution function.
* \\(T\\left(\\dots\\right)\\), the Student t cumulative distribution function.
* \\(n_c\\), the number of comparisons (pairs)
References
----------
Benavoli, A., Corani, G., & Mangili, F. (2016). Should we really use post-hoc tests based on mean-ranks? *Journal of Machine Learning Research, 17*, 1–10. doi:10.48550/ARXIV.1505.02288
BrightStat. (n.d.). Friedman test. BrightStat. Retrieved November 5, 2023, from https://secure.brightstat.com/index.php?p=c&d=1&c=2&i=9
Conover, W. J. (1980). *Practical nonparametric statistics* (2nd ed.). Wiley.
Demšar, J. (2006). Statistical comparisons of classifiers over multiple data sets. *The Journal of Machine Learning Research, 7*, 1–30. doi:10.5555/1248547.1248548
Gnambs, T. (n.d.). SPSS Friedman. http://timo.gnambs.at/sites/default/files/spss_friedmanph.sps
IBM. (2021). IBM SPSS Statistics Algorithms. IBM.
Nemenyi, P. (1963). *Distribution-free Multiple Comparisons*. Princeton University.
Pohlert, T. (2016). The pairwise multiple comparison of mean ranks package (PMCMR). https://cran.r-hub.io/web/packages/PMCMR/vignettes/PMCMR.pdf
Author
------
Made by P. Stikker
Companion website: https://PeterStatistics.com
YouTube channel: https://www.youtube.com/stikpet
Donations: https://www.patreon.com/bePatron?u=19398076
'''
#Remove rows with missing values and reset index
data = data.dropna()
data = data.reset_index(drop=True)
nr = len(data)
k = len(data.columns)
if levels is not None:
data = data.replace(levels)
ranks = pd.DataFrame()
for i in range(0, nr):
rankRow = pd.Series(rankdata(data.iloc[i, :]))
ranks[i] = rankRow
ranks = ranks.transpose()
rs = ranks.sum().sum()
rm = rs/(nr*k)
#Determine for each variable the average rank, and
#the squared difference of this average with the overall average rank.
rmj = ranks.sum()/nr
rs2 = (ranks**2).sum().sum()
sst = nr*((rmj - rm)**2).sum()
sse = ranks.stack().var(ddof=0)*k/(k-1)
if ties:
qadj = sst / sse
else:
qadj = 12 / (nr * k * (k + 1)) * rs2 - 3 * nc * (k + 1)
ncomp = k*(k - 1)/2
res = pd.DataFrame([[0]*7]*int(ncomp))
useRow=0
for i in range(0, k-1):
for j in range(i+1, k):
res.iloc[useRow,0] = data.columns[i]
res.iloc[useRow,1] = data.columns[j]
res.iloc[useRow,2] = nr
useRow = useRow + 1
useRow=0
if method=="dunn":
se = (k * (k + 1) / (6 * nr))**0.5
df = nr - k
for i in range(0, k-1):
for j in range(i+1, k):
z = (rmj[i] - rmj[j])/se
res.iloc[useRow,3] = z
res.iloc[useRow,4] = None
res.iloc[useRow,5] = 2 * (1 - NormalDist().cdf(abs(z)))
useRow = useRow + 1
elif method == "conover":
r2 = ((rmj * nr)**2).sum()
se = (2 * (nr * rs2 - r2) / ((nr - 1) * (k - 1)))**0.5
df = (nr - 1) * (k - 1)
for i in range(0, k-1):
for j in range(i+1, k):
tVal = (rmj[i] - rmj[j]) * nr / se
res.iloc[useRow,3] = tVal
res.iloc[useRow,4] = df
res.iloc[useRow,5] = 2 * (1 - t.cdf(abs(tVal), df))
useRow = useRow + 1
elif method == "nemenyi":
se = (k * (k + 1) / (6 * nr))**0.5
df = nr - k
for i in range(0, k-1):
for j in range(i+1, k):
q = (rmj[i] - rmj[j]) / se * (2**0.5)
res.iloc[useRow,3] = q
res.iloc[useRow,4] = df
res.iloc[useRow,5] = 1 - studentized_range.cdf(abs(q), k=k, df=df, scale=1)
useRow = useRow + 1
useRow=0
for i in range(0, k-1):
for j in range(i+1, k):
res.iloc[useRow,6] = res.iloc[useRow, 5]*ncomp
if res.iloc[useRow,6] > 1:
res.iloc[useRow,6] = 1
useRow = useRow + 1
res.columns = ["field 1", "field 2", "n", "statistic", "df", "p-value", "adj. p-value"]
return res
Functions
def ph_friedman(data, levels=None, method='dunn', ties=True)
-
Post-Hoc Tests for a Friedman Test
A post-hoc test after a Friedman test can be used to determine which variables differ significantly.
This function provides three options: Dunn, Conover, and Nemenyi.
Parameters
data
:dataframe
- dataframe with a column for each variable
levels
:dataframe
ordictionary
, optional- indication of what the levels are in order
method
:{"dunn", "conover", "nemenyi"}
, optional- Post-Hoc method. Default is "dunn"
ties
:boolean
, optional- apply a ties correction. Default is True
Returns
res
:dataframe
- with the following columns
- field 1,
- field 2,
- n, sample size
- *statistic", test statistic used
- df, degrees of freedom (if applicable)
- p-value, the p-value (significance)
- adj. p-value, Bonferroni adjusted p-value
Notes
Conover
Bartz-Beielstein et al. (2010, p. 319) attributes this to Conover (1999) (but also seen sites refering to Conover (1980), just different editions of the book) and uses as a formula: t_{1,2} = \frac{\left|R_1 - R_2\right|}{SE} SE = \sqrt{\frac{2\times n\times\left(1-\frac{\chi_F^2}{n\times\left(k-1\right)}\right)\times\left(\sum_{i=1}^n\sum_{j=1}^k r_{i,j}^2 - \frac{n\times k\times\left(k+1\right)^2}{4}\right)}{\left(n-1\right)\times\left(k-1\right)}}
With: R_j = \sum_{i=1}^n r_{i,j}
In the original source it mentions 2\times k in SE instead of 2\times n, this was indeed an error (pers. comm. with Conover).
Gnambs (n.d.) and BrightStat (n.d.) show a different formula, that gives the same result, and is the one the function uses: SE = \sqrt{\frac{2\times\left(\sum_{i=1}^n\sum_{j=1}^k r_{i,j}^2 - \sum_{j=1}^k R_j^2\right)}{\left(n-1\right)\times\left(k-1\right)}}
The significance is then determined using: sig. = 2\times\left(1 - T\left(\left|t_{i,2}\right|, df\right)\right)
Note that in the calculation SE is determined using all ranks, including those not in the comparison.
Nemenyi
Pohlert (2016, p. 15) shows the formula from Nemenyi (1963) as well as in Demšar (2006, pp. 11-12): q_{1,2} = \frac{\left|R_1 - R_2\right|}{\sqrt{\frac{k\times\left(k+1\right)}{6\times n}}}\times\sqrt{2} df = n - k
This follows then a studentized range distribution with: sig. = 1 - Q\left(q_{1,2}, k, df\right)
Dunn
Benavoli et. al (2016, pp. 2-3) and IBM SPSS (2021, p. 814): z_{1,2} = \frac{\left|R_1 - R_2\right|}{SE} SE = \sqrt{\frac{k\times\left(k+1\right)}{6\times n}}
This follows a standard normal distribution: sig. = 2\times\left(1 - \Phi\left(\left|z_{i,2}\right|\right)\right)
Bonferroni adjustment
The Bonferroni adjustment is done using: sig._{adj} = \min \left(sig. \times n_c, 1\right) n_c = \frac{k\times\left(k-1\right)}{2}
Symbols Used
- n, the number of cases
- k, the number of variables
- r_{i,j}, the rank of case i, in variable j. The ranks are determined for each case.
- \Phi\left(\dots\right), the standard normal cumulative distribution function.
- Q\left(\dots\right), the studentized range distribution cumulative distribution function.
- T\left(\dots\right), the Student t cumulative distribution function.
- n_c, the number of comparisons (pairs)
References
Benavoli, A., Corani, G., & Mangili, F. (2016). Should we really use post-hoc tests based on mean-ranks? Journal of Machine Learning Research, 17, 1–10. doi:10.48550/ARXIV.1505.02288
BrightStat. (n.d.). Friedman test. BrightStat. Retrieved November 5, 2023, from https://secure.brightstat.com/index.php?p=c&d=1&c=2&i=9
Conover, W. J. (1980). Practical nonparametric statistics (2nd ed.). Wiley.
Demšar, J. (2006). Statistical comparisons of classifiers over multiple data sets. The Journal of Machine Learning Research, 7, 1–30. doi:10.5555/1248547.1248548
Gnambs, T. (n.d.). SPSS Friedman. http://timo.gnambs.at/sites/default/files/spss_friedmanph.sps
IBM. (2021). IBM SPSS Statistics Algorithms. IBM.
Nemenyi, P. (1963). Distribution-free Multiple Comparisons. Princeton University.
Pohlert, T. (2016). The pairwise multiple comparison of mean ranks package (PMCMR). https://cran.r-hub.io/web/packages/PMCMR/vignettes/PMCMR.pdf
Author
Made by P. Stikker
Companion website: https://PeterStatistics.com
YouTube channel: https://www.youtube.com/stikpet
Donations: https://www.patreon.com/bePatron?u=19398076Expand source code
def ph_friedman(data, levels=None, method = "dunn", ties=True): ''' Post-Hoc Tests for a Friedman Test ---------------------------------- A post-hoc test after a Friedman test can be used to determine which variables differ significantly. This function provides three options: Dunn, Conover, and Nemenyi. Parameters ---------- data : dataframe dataframe with a column for each variable levels : dataframe or dictionary, optional indication of what the levels are in order method : {"dunn", "conover", "nemenyi"}, optional Post-Hoc method. Default is "dunn" ties : boolean, optional apply a ties correction. Default is True Returns ------- res : dataframe with the following columns * *field 1*, * *field 2*, * *n*, sample size * *statistic", test statistic used * *df*, degrees of freedom (if applicable) * *p-value*, the p-value (significance) * *adj. p-value*, Bonferroni adjusted p-value Notes ----- **Conover** Bartz-Beielstein et al. (2010, p. 319) attributes this to Conover (1999) (but also seen sites refering to Conover (1980), just different editions of the book) and uses as a formula: $$t_{1,2} = \\frac{\\left|R_1 - R_2\\right|}{SE}$$ $$SE = \\sqrt{\\frac{2\\times n\\times\\left(1-\\frac{\\chi_F^2}{n\\times\\left(k-1\\right)}\\right)\\times\\left(\\sum_{i=1}^n\\sum_{j=1}^k r_{i,j}^2 - \\frac{n\\times k\\times\\left(k+1\\right)^2}{4}\\right)}{\\left(n-1\\right)\\times\\left(k-1\\right)}}$$ With: $$R_j = \\sum_{i=1}^n r_{i,j}$$ In the original source it mentions \\(2\\times k\\) in \\(SE\\) instead of \\(2\\times n\\), this was indeed an error (pers. comm. with Conover). Gnambs (n.d.) and BrightStat (n.d.) show a different formula, that gives the same result, and is the one the function uses: $$SE = \\sqrt{\\frac{2\\times\\left(\\sum_{i=1}^n\\sum_{j=1}^k r_{i,j}^2 - \\sum_{j=1}^k R_j^2\\right)}{\\left(n-1\\right)\\times\\left(k-1\\right)}}$$ The significance is then determined using: $$sig. = 2\\times\\left(1 - T\\left(\\left|t_{i,2}\\right|, df\\right)\\right)$$ Note that in the calculation \\(SE\\) is determined using all ranks, including those not in the comparison. **Nemenyi** Pohlert (2016, p. 15) shows the formula from Nemenyi (1963) as well as in Demšar (2006, pp. 11-12): $$q_{1,2} = \\frac{\\left|R_1 - R_2\\right|}{\\sqrt{\\frac{k\\times\\left(k+1\\right)}{6\\times n}}}\\times\\sqrt{2}$$ $$df = n - k$$ This follows then a studentized range distribution with: $$sig. = 1 - Q\\left(q_{1,2}, k, df\\right)$$ **Dunn** Benavoli et. al (2016, pp. 2-3) and IBM SPSS (2021, p. 814): $$z_{1,2} = \\frac{\\left|R_1 - R_2\\right|}{SE}$$ $$SE = \\sqrt{\\frac{k\\times\\left(k+1\\right)}{6\\times n}}$$ This follows a standard normal distribution: $$sig. = 2\\times\\left(1 - \\Phi\\left(\\left|z_{i,2}\\right|\\right)\\right)$$ **Bonferroni adjustment** The Bonferroni adjustment is done using: $$sig._{adj} = \\min \\left(sig. \\times n_c, 1\\right)$$ $$n_c = \\frac{k\\times\\left(k-1\\right)}{2}$$ *Symbols Used* * \\(n\\), the number of cases * \\(k\\), the number of variables * \\(r_{i,j}\\), the rank of case i, in variable j. The ranks are determined for each case. * \\(\\Phi\\left(\\dots\\right)\\), the standard normal cumulative distribution function. * \\(Q\\left(\\dots\\right)\\), the studentized range distribution cumulative distribution function. * \\(T\\left(\\dots\\right)\\), the Student t cumulative distribution function. * \\(n_c\\), the number of comparisons (pairs) References ---------- Benavoli, A., Corani, G., & Mangili, F. (2016). Should we really use post-hoc tests based on mean-ranks? *Journal of Machine Learning Research, 17*, 1–10. doi:10.48550/ARXIV.1505.02288 BrightStat. (n.d.). Friedman test. BrightStat. Retrieved November 5, 2023, from https://secure.brightstat.com/index.php?p=c&d=1&c=2&i=9 Conover, W. J. (1980). *Practical nonparametric statistics* (2nd ed.). Wiley. Demšar, J. (2006). Statistical comparisons of classifiers over multiple data sets. *The Journal of Machine Learning Research, 7*, 1–30. doi:10.5555/1248547.1248548 Gnambs, T. (n.d.). SPSS Friedman. http://timo.gnambs.at/sites/default/files/spss_friedmanph.sps IBM. (2021). IBM SPSS Statistics Algorithms. IBM. Nemenyi, P. (1963). *Distribution-free Multiple Comparisons*. Princeton University. Pohlert, T. (2016). The pairwise multiple comparison of mean ranks package (PMCMR). https://cran.r-hub.io/web/packages/PMCMR/vignettes/PMCMR.pdf Author ------ Made by P. Stikker Companion website: https://PeterStatistics.com YouTube channel: https://www.youtube.com/stikpet Donations: https://www.patreon.com/bePatron?u=19398076 ''' #Remove rows with missing values and reset index data = data.dropna() data = data.reset_index(drop=True) nr = len(data) k = len(data.columns) if levels is not None: data = data.replace(levels) ranks = pd.DataFrame() for i in range(0, nr): rankRow = pd.Series(rankdata(data.iloc[i, :])) ranks[i] = rankRow ranks = ranks.transpose() rs = ranks.sum().sum() rm = rs/(nr*k) #Determine for each variable the average rank, and #the squared difference of this average with the overall average rank. rmj = ranks.sum()/nr rs2 = (ranks**2).sum().sum() sst = nr*((rmj - rm)**2).sum() sse = ranks.stack().var(ddof=0)*k/(k-1) if ties: qadj = sst / sse else: qadj = 12 / (nr * k * (k + 1)) * rs2 - 3 * nc * (k + 1) ncomp = k*(k - 1)/2 res = pd.DataFrame([[0]*7]*int(ncomp)) useRow=0 for i in range(0, k-1): for j in range(i+1, k): res.iloc[useRow,0] = data.columns[i] res.iloc[useRow,1] = data.columns[j] res.iloc[useRow,2] = nr useRow = useRow + 1 useRow=0 if method=="dunn": se = (k * (k + 1) / (6 * nr))**0.5 df = nr - k for i in range(0, k-1): for j in range(i+1, k): z = (rmj[i] - rmj[j])/se res.iloc[useRow,3] = z res.iloc[useRow,4] = None res.iloc[useRow,5] = 2 * (1 - NormalDist().cdf(abs(z))) useRow = useRow + 1 elif method == "conover": r2 = ((rmj * nr)**2).sum() se = (2 * (nr * rs2 - r2) / ((nr - 1) * (k - 1)))**0.5 df = (nr - 1) * (k - 1) for i in range(0, k-1): for j in range(i+1, k): tVal = (rmj[i] - rmj[j]) * nr / se res.iloc[useRow,3] = tVal res.iloc[useRow,4] = df res.iloc[useRow,5] = 2 * (1 - t.cdf(abs(tVal), df)) useRow = useRow + 1 elif method == "nemenyi": se = (k * (k + 1) / (6 * nr))**0.5 df = nr - k for i in range(0, k-1): for j in range(i+1, k): q = (rmj[i] - rmj[j]) / se * (2**0.5) res.iloc[useRow,3] = q res.iloc[useRow,4] = df res.iloc[useRow,5] = 1 - studentized_range.cdf(abs(q), k=k, df=df, scale=1) useRow = useRow + 1 useRow=0 for i in range(0, k-1): for j in range(i+1, k): res.iloc[useRow,6] = res.iloc[useRow, 5]*ncomp if res.iloc[useRow,6] > 1: res.iloc[useRow,6] = 1 useRow = useRow + 1 res.columns = ["field 1", "field 2", "n", "statistic", "df", "p-value", "adj. p-value"] return res