diff --git a/README.md b/README.md index 20f23611..e69ee4d0 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,11 @@ Functions are grouped into: - ICH/MedDRA: `health.emt` - WHODrug: - Standard: - - CDISC*: `stdcdisc.lib` + - CDISC[*]: `stdcdisc.lib` - ISO: `stdiso.pdfsummary` -- StatLab*: - - Correlation: `statlab.corr` - - Reliability: `statlab.kappa` +- StatLab has been moved to a new package `mtbp3Lab` after v0.2.21 and will be extend to provide a broarder range of practicing topics. Enjoy! -* Documents are not executed while building. +[*] Documents are not executed while building. ## Table of Contents @@ -87,4 +85,4 @@ Hsu, Y. (2024). mtbp3: My tool box in Python [Software]. Retrieved from https:// url = {https://yh202109.github.io/mtbp3/index.html}, note = {Software} } -``` \ No newline at end of file +``` diff --git a/docs/index.md b/docs/index.md index 5a31424f..bdb98d8a 100755 --- a/docs/index.md +++ b/docs/index.md @@ -17,10 +17,6 @@ example_emt4.ipynb std_iso_pdf.ipynb std_iso_idmp.ipynb std_cdisc.ipynb -statlab_kappa.rst -statlab_kappa2.rst -statlab_corr_tau.rst -statlab_corr_spearman_rho.rst changelog.md contributing.md diff --git a/docs/statlab_corr_spearman_rho.rst b/docs/statlab_corr_spearman_rho.rst deleted file mode 100644 index fc48916c..00000000 --- a/docs/statlab_corr_spearman_rho.rst +++ /dev/null @@ -1,164 +0,0 @@ -.. - # Copyright (C) 2023-2024 Y Hsu - # - # This program is free software: you can redistribute it and/or modify - # it under the terms of the GNU General Public license as published by - # the Free software Foundation, either version 3 of the License, or - # any later version. - # - # This program is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - # GNU General Public License for more details - # - # You should have received a copy of the GNU General Public license - # along with this program. If not, see - -.. role:: red-b - -.. role:: red - -############# -StatLab/Corr/NP/Spearman's Rho -############# - -:red-b:`Disclaimer:` -:red:`This page is provided only for studying and practicing. The author does not intend to promote or advocate any particular analysis method or software.` - -************* -Background -************* - -Spearman's rho (:math:`\rho`) is a statistic used for measuring rank correlation [1]_ . - -************* -Notation -************* - -Let :math:`(Y_{i1}, Y_{i2})` be a pair of random variables corresponding to the :math:`i` th sample where :math:`i = 1, \ldots, n`. - -.. list-table:: Observed Value - :widths: 10 10 10 - :header-rows: 1 - :name: tbl_count1 - - * - - - :math:`Y_{i1}` - - :math:`Y_{i2}` - * - **Sample:** 1 - - :math:`Y_{11}` - - :math:`Y_{12}` - * - **Sample:** 2 - - :math:`Y_{21}` - - :math:`Y_{22}` - * - :math:`\vdots` - - :math:`\vdots` - - :math:`\vdots` - * - **Sample:** :math:`n` - - :math:`Y_{n1}` - - :math:`Y_{n2}` - -Let :math:`(R_{i1}, R_{i2})` be the rank of :math:`Y_{i1}` and the rank of :math:`Y_{i2}`. -In the case of ties, one method is to assign the tied group with the average of unique ranks corresponding the tied group. -For the :math:`i` th sample, let -:math:`S_{i1,1}` be the number of observed values less than :math:`Y_{i1}`, -:math:`S_{i1,2}` be the number of observed values equal to :math:`Y_{i1}`, -and :math:`S_{i1,3}` be the number of observed values greater to :math:`Y_{i1}`. -We can calculate the rank of a single sample as - -.. math:: - :label: eq_rank - - R_{i1} = S_{i1,1} + \frac{S_{i1,2}+1}{2} = n - S_{i1,3} - \frac{S_{i1,2}-1}{2}. - -For a vector, ``pandas.DataFrame`` has the ``rank`` function with ``method='average'`` option to calculate rank as defined in :eq:`eq_rank`. -In ``R``, that can be calculated using the ``rank`` function with ``ties.method='average'`` option. -See reference [2]_ for ranking in ``Julia``. - -The Spearman's :math:`\rho` can be calculated as: - -.. math:: - :label: eq_rho - - \rho = \frac{\frac{1}{n}\sum_i R_{i1}R_{i2} - \frac{1}{4}(n+1)^2}{s_1 s_2}, - -where :math:`s_1^2 = \sum_i R_{i1}^2 - \frac{1}{4}(n+1)^2`, -and :math:`s_2^2 = \sum_i R_{i2}^2 - \frac{1}{4}(n+1)^2`. - -************* -Example - Group-1 -************* - -.. list-table:: Spearman's :math:`\rho = 1.0` - :widths: 10 10 10 - :header-rows: 1 - :name: tbl_ex1 - - * - - - :math:`Y_{i1}` - - :math:`Y_{i2}` - * - **Sample:** 1 - - 1 - - 4 - * - **Sample:** 2 - - 3 - - 6 - * - **Sample:** 3 - - 2 - - 5 - -.. list-table:: Spearman's :math:`\rho = -1.0` - :widths: 10 10 10 - :header-rows: 1 - :name: tbl_ex1 - - * - - - :math:`Y_{i1}` - - :math:`Y_{i2}` - * - **Sample:** 1 - - 1 - - 6 - * - **Sample:** 2 - - 3 - - 4 - * - **Sample:** 3 - - 2 - - 5 - -************* -How-to -************* - -To use ``scipy.stats`` [3]_: - -.. code:: python - - from scipy.stats import spearmanr - - y1 = [1, 3, 2] - y2 = [4, 6, 5] - - rho, p_value = spearmanr(y1, y2) - print("Spearman's rho:", rho) - -************* -More Details -************* - -Assume that :math:`Y_{i1} \sim \mathcal{D}`. -For continuous :math:`Y_{i1}`, if we can assume -that :math:`P(S_{i1,2}=1)=1` for all :math:`i`, -then :eq:`eq_rank` can be simplified as :math:`R_{i1} = S_{i1,1}+1`. -For a given sample size :math:`n`, and :math:`r \in \{1, \ldots, n\}`, the pmf of :math:`R_{i1}` is -:math:`P(R_{i1} = r) = \frac{1}{n}`, which does not depend on :math:`r` or :math:`\mathcal{D}` [4]_. - - -************* -Reference -************* - -.. [1] Wikipedia. (year). Spearman's rank correlation coefficient. https://en.wikipedia.org/wiki/Spearman%27s_rank_correlation_coefficient -.. [2] julialang.org. (2022). Ranking of elements of a vector. https://discourse.julialang.org/t/ranking-of-elements-of-a-vector/88293/4 -.. [3] scipy.org. (year). spearmanr. https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.spearmanr.html -.. [4] John Borkowski. (2014). Introduction to the Theory of Order Statistics and Rank Statistics. https://math.montana.edu/jobo/thainp/rankstat.pdf - diff --git a/docs/statlab_corr_tau.rst b/docs/statlab_corr_tau.rst deleted file mode 100644 index 86b664ce..00000000 --- a/docs/statlab_corr_tau.rst +++ /dev/null @@ -1,211 +0,0 @@ -.. - # Copyright (C) 2023-2024 Y Hsu - # - # This program is free software: you can redistribute it and/or modify - # it under the terms of the GNU General Public license as published by - # the Free software Foundation, either version 3 of the License, or - # any later version. - # - # This program is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - # GNU General Public License for more details - # - # You should have received a copy of the GNU General Public license - # along with this program. If not, see - -.. role:: red-b - -.. role:: red - -############# -StatLab/Corr/NP/Kendall's Tau -############# - -:red-b:`Disclaimer:` -:red:`This page is provided only for studying and practicing. The author does not intend to promote or advocate any particular analysis method or software.` - -************* -Background -************* - -Kendall's tau (:math:`\tau`) is a statistic used for measuring rank correlation [1]_ [2]_. - -************* -Notation -************* - -Let :math:`(Y_{i1}, Y_{i2})` be a pair of random variables corresponding to the :math:`i` th sample where :math:`i = 1, \ldots, n`. - -.. list-table:: Observed Value - :widths: 10 10 10 - :header-rows: 1 - :name: tbl_count1 - - * - - - :math:`Y_{i1}` - - :math:`Y_{i2}` - * - **Sample:** 1 - - :math:`Y_{11}` - - :math:`Y_{12}` - * - **Sample:** 2 - - :math:`Y_{21}` - - :math:`Y_{22}` - * - :math:`\vdots` - - :math:`\vdots` - - :math:`\vdots` - * - **Sample:** :math:`n` - - :math:`Y_{n1}` - - :math:`Y_{n2}` - -Let :math:`Z_{ij1} \equiv sign(Y_{i1}-Y_{j1})`, :math:`Z_{ij2} \equiv sign(Y_{i2}-Y_{j2})`, -:math:`c = \sum_{i=1}^n \sum_{j < i} I(Z_{ij1}Z_{ij2}=1)`, -:math:`d = \sum_{i=1}^n \sum_{j < i} I(Z_{ij1}Z_{ij2}=-1)` -and :math:`t = \frac{n(n-1)}{2}`. -The coefficient :math:`\tau` (tau-a) can be calculated as - -.. math:: - :label: eq_tau1 - - \tau = \frac{ c - d }{t}. - -If there are no ties, the maximum value of :eq:`eq_tau1` is 1 at :math:`c=t`, -and the minimum is -1 at :math:`d=t`. - -:eq:`eq_tau1` can also be expressed as - -.. math:: - :label: eq_tau2 - - \tau =& \frac{2}{n(n-1)} \left( \sum_{i=1}^n \sum_{j < i} Z_{ij1}Z_{ij2} \right) \\ - =& \frac{1}{n(n-1)} \left( \sum_{i=1}^n \sum_{j=1}^n Z_{ij1}Z_{ij2} \right). - -Under independent sample assumption, for a fixed :math:`n`, we know that -:math:`E(Z_{ij1})=E(Z_{ij2})=0` and -:math:`Var(Z_{ij1})=Var(Z_{ij2})=1-\frac{1}{n}`. -From :eq:`eq_tau2`, we can see that :math:`\tau` is a type of correlation coefficient. - -If there are ties, the maximum value of :eq:`eq_tau1` becomes less then 1. -Consider the scenario that there are :math:`n_{t1}` groups of ties in :math:`\{Y_{i1}\}`, -and there are :math:`n_{t2}` groups of ties in :math:`\{Y_{i2}\}`. -Let :math:`n_{t1,k}` be the number of ties within the :math:`k` th group of ties in :math:`\{Y_{i1}\}`, -and :math:`n_{t2,k}` be the number of ties within the :math:`k` th group of ties in :math:`\{Y_{i2}\}` -The adjusted :math:`\tau` (tau-b) is calculated by replacing :math:`t` in :eq:`eq_tau1` with -:math:`t^* = \sqrt{\frac{1}{2}n(n-1)-\sum_{k=1}^{n_{t1}} \frac{1}{2}n_{t1,k}(n_{t1,k}-1)}\sqrt{\frac{1}{2}n(n-1)-\sum_{k=1}^{n_{t2}} \frac{1}{2}n_{t2,k}(n_{t2,k}-1)}` - -************* -Example - Group-1 -************* - -.. list-table:: Kendall's :math:`\tau = 1.0` - :widths: 10 10 10 - :header-rows: 1 - :name: tbl_ex1 - - * - - - :math:`Y_{i1}` - - :math:`Y_{i2}` - * - **Sample:** 1 - - 1 - - 4 - * - **Sample:** 2 - - 3 - - 6 - * - **Sample:** 3 - - 2 - - 5 - -.. list-table:: Kendall's :math:`\tau = -1.0` - :widths: 10 10 10 - :header-rows: 1 - :name: tbl_ex1 - - * - - - :math:`Y_{i1}` - - :math:`Y_{i2}` - * - **Sample:** 1 - - 1 - - 6 - * - **Sample:** 2 - - 3 - - 4 - * - **Sample:** 3 - - 2 - - 5 - -************* -How-to -************* - -To use ``scipy.stats`` [3]_: - -.. code:: python - - from scipy.stats import kendalltau - y1 = [1,3,2] - y2 = [4,6,5] - - tau, p_value = kendalltau(y1, y2) - print("Kendall's tau:", tau) - -************* -Lab Exercise -************* - -1. Show :math:`E(Z_{ij})=0`. - -************* -Algorithm - 1 -************* - -**WARNING: FOR SMALL SAMPLE SIZES ONLY** - -Note that the algorithm in this section is implement in ``mtbp3.stalab`` for illustration purpose. -Although the matrix form is closely representing :eq:`eq_tau2`, -the calculation time increases greatly when the sample size increases. -Other algorithms can be found in references. - -Let :math:`Y_{1} = (Y_{11}, \ldots, Y_{n1})` and :math:`Y_{2} = (Y_{12}, \ldots, Y_{n2})`. -Let :math:`\times` represent the matrix product, -:math:`\times_{car}` represent the Cartesian product, -:math:`\times_{ele}` represent the element-wise product, -:math:`g([(a,b)]) = [sign(a-b)]`. -and :math:`h(X_n) = 1_n \times X_n \times 1_n^T` -where :math:`X_n` is a size :math:`n` by :math:`n` matrix, and :math:`1_n` is a length :math:`n` one vector. -Both tau-a and tau-b can be calculated using the following steps: - -1. calculate components :math:`\tau_1 = g(Y_{1} \times_{car} Y_{1})` and :math:`\tau_2 = g(Y_{2} \times_{car} Y_{2})` -2. calculate :math:`\tau` as :math:`\tau = \frac{h(\tau_1 \times_{ele} \tau_2) }{ \sqrt{h(abs(\tau_1))}\sqrt{h(abs(\tau_2))} }` - -============= -How-to -============= - -To use ``mtbp3.corr``: - -.. code:: python - - import numpy as np - from mtbp3.corr import CorrCalculator - - size = 100 - y1 = np.random.randint(1, size+1, size=size).tolist() - y2 = np.subtract(np.random.randint(1, size+1, size=size),y1).tolist() - t = CorrCalculator([y1,y2]) - print("Kendall's tau (mtbp3.corr):", t.calculate_kendall_tau()) - -To create a scatter plot of ``y1`` and ``y2``: - -.. code:: python - - t.plot_y_list(axis_label=['y1','y2']) - - -************* -Reference -************* - -.. [1] Wikipedia. (year). Kendall rank correlation coefficient. https://en.wikipedia.org/wiki/Kendall_rank_correlation_coefficient -.. [2] Encyclopedia of Mathematics. (yeawr). Kendall tau metric. https://encyclopediaofmath.org/index.php?title=Kendall_tau_metric -.. [3] Scipy. (year). kendalltau. https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.kendalltau.html - diff --git a/docs/statlab_kappa.rst b/docs/statlab_kappa.rst deleted file mode 100644 index a2823b92..00000000 --- a/docs/statlab_kappa.rst +++ /dev/null @@ -1,511 +0,0 @@ -.. - # Copyright (C) 2023-2024 Y Hsu - # - # This program is free software: you can redistribute it and/or modify - # it under the terms of the GNU General Public license as published by - # the Free software Foundation, either version 3 of the License, or - # any later version. - # - # This program is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - # GNU General Public License for more details - # - # You should have received a copy of the GNU General Public license - # along with this program. If not, see - - -.. role:: red-b - -.. role:: red - -.. role:: bg-ltsteelblue - - - -############# -StatLab/Reli/Cohen's Kappa -############# - -:red-b:`Disclaimer:` -:red:`This page is provided only for studying and practicing. The author does not intend to promote or advocate any particular analysis method or software.` - -************* -Background -************* - -Cohen's kappa (:math:`\kappa`) is a statistic used for describing inter-ratter reliability of two ratters (or intra-rater) with categorical rating outcomes [1]_. -Please note that there are also additional considerations for the use of :math:`\kappa` for quantifying agreement [2]_ [3]_ . - -************* -Notation -************* - -For two ratters and two categories rating, let :math:`Y_{r,i} \in \{v_j; j=1,2\}` represent rating -from rater :math:`r=1,2` for sample :math:`i = 1, \ldots, n`. -Let :math:`N_{j_1,j_2}` represent the total number of sample received ratings :math:`(v_{j_1}, v_{j_2})` from two raters, where :math:`j_1,j_2 \in \{1,2\}`. - -.. list-table:: Counts for 2 categories - :widths: 10 10 10 10 - :header-rows: 1 - - * - - - Ratter 2: :math:`v_1` - - Ratter 2: :math:`v_2` - - Row Total - * - **Ratter 1:** :math:`v_1` - - :math:`N_{11}` - - :math:`N_{12}` - - :math:`N_{1\bullet}` - * - **Ratter 1:** :math:`v_2` - - :math:`N_{21}` - - :math:`N_{22}` - - :math:`N_{2\bullet}` - * - **Column Total** - - :math:`N_{\bullet 1}` - - :math:`N_{\bullet 2}` - - :math:`n` - -For two ratters and three or more categories rating, let :math:`Y_{r,i} \in \{v_1,v_2,v_3, \ldots, v_J \}` represent rating -from rater :math:`r=1,2` for sample :math:`i = 1, \ldots, n`. -Let :math:`N_{j_1,j_2}` represent the total number of sample received ratings :math:`(v_{j_1}, v_{j_2})` from two raters, where :math:`j_1,j_2 \in \{1,\ldots,J\}`. - -.. list-table:: Counts for 3 or more categories - :widths: 10 10 10 10 10 10 - :header-rows: 1 - - * - - - Ratter 2: :math:`v_1` - - Ratter 2: :math:`v_2` - - Ratter 2: :math:`v_3` - - :math:`\ldots` - - Row Total - * - **Ratter 1:** :math:`v_1` - - :math:`N_{11}` - - :math:`N_{12}` - - :math:`N_{13}` - - :math:`\ldots` - - :math:`N_{1\bullet}` - * - **Ratter 1:** :math:`v_2` - - :math:`N_{21}` - - :math:`N_{22}` - - :math:`N_{23}` - - :math:`\ldots` - - :math:`N_{2\bullet}` - * - **Ratter 1:** :math:`v_3` - - :math:`N_{31}` - - :math:`N_{32}` - - :math:`N_{33}` - - :math:`\ldots` - - :math:`N_{3\bullet}` - * - :math:`\vdots` - - :math:`\vdots` - - :math:`\vdots` - - :math:`\vdots` - - :math:`\ddots` - - :math:`\vdots` - * - **Column Total** - - :math:`N_{\bullet 1}` - - :math:`N_{\bullet 2}` - - :math:`N_{\bullet 3}` - - :math:`\ldots` - - :math:`n` - -The observed raw percentage of agreement is defined as - -.. math:: - - p_O = \sum_{j=1}^J N_{jj} / n - -where :math:`J \geq 2` is the size of value set. - -Assume that - -.. math:: - (N_{1\bullet}, \ldots N_{J\bullet}) \sim multi(n, (p_{r=1,1}, \ldots, p_{r=1,J})), - -and - -.. math:: - (N_{\bullet 1}, \ldots N_{\bullet J}) \sim multi(n, (p_{r=2,1}, \ldots, p_{r=2,J})), - -with :math:`\sum_j N_{j \bullet} = \sum_j N_{\bullet j} = n` -and :math:`\sum_j p_{r=1,j} = \sum_j p_{r=2, j} = 1`. - -Under independence assumption, the expected number of agreement is estimated by -:math:`\sum_{j=1}^J\hat{E}_{j} = \frac{1}{n}\sum_{j=1}^J N_{\bullet j} N_{j\bullet} \equiv n p_E`. - -The Cohen's :math:`\kappa` statistic is calculated as - -.. math:: - \kappa = \frac{p_O - p_E}{1-p_E}. - -The SE of :math:`\kappa` is calculated as - -.. math:: - \sqrt{\frac{p_O(1-p_O)}{n(1-p_E)^2}}. - -************* -Interpretation of Cohen's Kappa Suggested in Literature -************* - -There are several groups of interpretation. Some roughly (not-strictly) defined types are listed below: - -1. Table based interpretation: a shared interpretation simplifies application process and provides a easy to compare values. -2. Interpretation based on Approximated model based confidence interval or Bootstrap confidence intervals with a preselected criterion -3. Bayesian inference based interpretation [8]_ - -Cohen (1960) [4]_ suggested the Kappa result be interpreted as follows: - -.. list-table:: Cohen's Kappa Interpretation (Cohen, 1960) - :widths: 10 10 - :header-rows: 1 - - * - Value of :math:`\kappa` - - Interpretation - * - :math:`-1 \leq \kappa \leq 0` - - indicating no agreement - * - :math:`0 < \kappa \leq 0.2` - - none to slight - * - :math:`0.2 < \kappa \leq 0.4` - - fair - * - :math:`0.4 < \kappa \leq 0.6` - - moderate - * - :math:`0.6 < \kappa \leq 0.8` - - substantial - * - :math:`0.8 < \kappa \leq 1` - - almost perfect agreement - -Interpretation suggested by McHugh (2012) [5]_: - -.. list-table:: Cohen's Kappa Interpretation (McHugh, 2012) - :widths: 10 10 10 - :header-rows: 1 - - * - Value of :math:`\kappa` - - Level of Agreement - - % of Data That Are Reliable - * - :math:`-1 \leq \kappa \leq 0` - - Disagreement - - NA - * - :math:`0-.20` - - None - - :math:`0-4%` - * - :math:`.21-.39` - - Minimal - - :math:`4-15%` - * - :math:`.40-.59` - - Weak - - :math:`15-35%` - * - :math:`.60-.79` - - Moderate - - :math:`35-63%` - * - :math:`.80-.90` - - Strong - - :math:`64-81%` - * - Above.90 - - Almost Perfect - - :math:`82-100%` - -As discussed by Sim and Wright [6]_ , biases and other factors could have impact on the interpretation. - -************* -Example - Group-1 -************* - -.. list-table:: Cohen's :math:`\kappa = 0` - :widths: 10 10 10 10 - :header-rows: 1 - - * - - - Ratter 2: :math:`v_1` - - Ratter 2: :math:`v_2` - - Row Total - * - **Ratter 1:** :math:`v_1` - - 9 - - 21 - - 30 - * - **Ratter 1:** :math:`v_2` - - 21 - - 49 - - 70 - * - **Column Total** - - 30 - - 70 - - 100 - -.. list-table:: Cohen's :math:`\kappa = 0` - :widths: 10 10 10 10 - :header-rows: 1 - - * - - - Ratter 2: :math:`v_1` - - Ratter 2: :math:`v_2` - - Row Total - * - **Ratter 1:** :math:`v_1` - - 49 - - 21 - - 70 - * - **Ratter 1:** :math:`v_2` - - 21 - - 9 - - 30 - * - **Column Total** - - 70 - - 30 - - 100 - -.. list-table:: Cohen's :math:`\kappa = 1` - :widths: 10 10 10 10 - :header-rows: 1 - - * - - - Ratter 2: :math:`v_1` - - Ratter 2: :math:`v_2` - - Row Total - * - **Ratter 1:** :math:`v_1` - - 30 - - 0 - - 30 - * - **Ratter 1:** :math:`v_2` - - 0 - - 70 - - 70 - * - **Column Total** - - 30 - - 70 - - 100 - -.. list-table:: Cohen's :math:`\kappa = 1` - :widths: 10 10 10 10 - :header-rows: 1 - - * - - - Ratter 2: :math:`v_1` - - Ratter 2: :math:`v_2` - - Row Total - * - **Ratter 1** :math:`v_1` - - 50 - - 0 - - 50 - * - **Ratter 1:** :math:`v_2` - - 0 - - 50 - - 50 - * - **Column Total** - - 50 - - 50 - - 100 - -.. list-table:: Cohen's :math:`\kappa = -1` - :widths: 10 10 10 10 - :header-rows: 1 - - * - - - Ratter 2: :math:`v_1` - - Ratter 2: :math:`v_2` - - Row Total - * - **Ratter 1:** :math:`v_1` - - 0 - - 50 - - 50 - * - **Ratter 1:** :math:`v_2` - - 50 - - 0 - - 50 - * - **Column Total** - - 50 - - 50 - - 100 - -.. list-table:: Cohen's :math:`\kappa = -0.7241379310344827` - :widths: 10 10 10 10 - :header-rows: 1 - - * - - - Ratter 2: :math:`v_1` - - Ratter 2: :math:`v_2` - - Row Total - * - **Ratter 1:** :math:`v_1` - - 0 - - 30 - - 30 - * - **Ratter 1:** :math:`v_2` - - 70 - - 0 - - 70 - * - **Column Total** - - 70 - - 30 - - 100 - - -************* -How-to -************* - -To use ``sklearn.metrics`` (stable): - -.. code:: python - - from sklearn.metrics import cohen_kappa_score - y1 = ['v2'] * 70 + ['v1'] * 30 - y2 = ['v1'] * 70 + ['v2'] * 30 - print("Cohen's kappa:", cohen_kappa_score(y1, y2)) - -To use ``mtbp3.statlab`` (testing): - -.. code:: python - - from mtbp3.statlab import kappa - y1 = ['v2'] * 70 + ['v1'] * 30 - y2 = ['v1'] * 70 + ['v2'] * 30 - kappa = kappa.KappaCalculator([y1,y2]) - print("Cohen's kappa:", kappa.cohen_kappa) - -============= -Bootstrap CI -============= - -To use ``mtbp3.statlab``: - -.. testsetup:: * - - from mtbp3.statlab import kappa - y1 = ['v2'] * 70 + ['v1'] * 30 - y2 = ['v1'] * 70 + ['v2'] * 30 - kappa = kappa.KappaCalculator(y1,y2) - -.. testcode:: - - print( kappa.bootstrap_cohen_ci(n_iterations=1000, confidence_level=0.95, out_digits=6) ) - -Output: - -.. testoutput:: - - Cohen's kappa: -0.724138 - Confidence Interval (95.0%): [-0.907669, -0.496558] - - -Note that examples of using ``SAS/PROC FREQ`` and ``R`` package ``vcd`` for calculating :math:`\kappa` can be found in reference [7]_ . - -============= -Bubble Plot -============= - -To create a bubble plot using ``mtbp3.statlab``: - -.. code:: python - - from mtbp3.statlab import kappa - - fruits = ['Apple', 'Orange', 'Pear'] - np.random.seed(100) - r1 = np.random.choice(fruits, size=100).tolist() - r2 = np.random.choice(fruits, size=100).tolist() - - kappa = KappaCalculator([r1,r2], stringna='NA') - print("Cohen's kappa (mtbp3.statlab): "+str(kappa.cohen_kappa)) - print("Number of raters per sample: "+str(kappa.n_rater)) - print("Number of rating categories: "+str(kappa.n_category)) - print("Number of sample: "+str(kappa.y_count.shape[0])) - - kappa.create_bubble_plot() - -Output: - -.. testoutput:: - - Cohen's kappa (mtbp3.statlab): 0.06513872135102527 - Number of raters per sample: 2.0 - Number of rating categories: 3 - Number of sample: 100 - -.. figure:: /_static/fig/statlab_kappa_fig1.svg - :align: center - :alt: bubble plot - -Sometimes monitoring individual raters rates might be needed for the interpretation of :math:`\kappa`. -To create a bubble plot with individual raters summary using ``mtbp3.statlab``: - -.. code:: python - - kappa.create_bubble_plot(hist=True) - -.. figure:: /_static/fig/statlab_kappa_fig2.svg - :align: center - :alt: bubble plot with hist - -Note that the agreed counts are on the 45 degree line. -To put agreed counts on the -45 degree line: - -.. code:: python - - kappa.create_bubble_plot(hist=True, reverse_y=True) - -.. figure:: /_static/fig/statlab_kappa_fig3.svg - :align: center - :alt: bubble plot with hist - reverse - -************* -Lab Exercise -************* - -Assume that there are two raters responsible for rating 2 studies with a sample size of 100 for each study. -Assume that the you are tasked with studying the characteristics of :math:`\kappa`. - -For the first study, the first rater completed the rating with marginal rates -following a multinomial distribution (100, (1/3, 1/3, 1/3)). -Afterwards, assume that you filled -a portion (:math:`0 < r < 1`) of the sample's ratings as a second rater with exactly the same rating as the first rater, -and filled out the rest with random ratings following the same distribution as the first rater. - -For the second study, the first rater completed the rating with marginal rates -following a multinomial distribution (100, (0.9, 0.05, 0.05)). -Afterwards, assume that you filled -a portion (:math:`0 < r < 1`) of the sample's ratings as a second rater with exactly the same rating as the first rater, -and filled out the rest with random ratings following the same distribution as the first rater. - -1. Find the relationship between :math:`r` and :math:`\kappa` for these two studies. - -************* -Extensions -************* - -Some scenarios discussed by Hallgren (2012) [9]_ include: - -- the **prevalence** problem: one category has much higher percentage than other categories and causes :math:`\kappa` to be low. -- the **bias** problem: there are substantial differences in marginal distributions and causes :math:`\kappa` tend to be high. -- unequal importance - -(Please note that this is not an exhaustive list.) - -************* -Weighted :math:`\kappa` -************* - -Let :math:`w_{j_1,j_2}` represent the weight given to total number of sample received ratings :math:`(v_{j_1}, v_{j_2})` from two raters, where :math:`j_1,j_2 \in \{1,\ldots,J\}`. -The weighted :math:`\kappa` is calculated as - -.. math:: - \kappa = 1- \frac{\sum_{j_1=1}^J\sum_{j_2=1}^J w_{j_1,j_2}N_{j_1,j_2}}{\sum_{j_1=1}^J\sum_{j_2=1}^J w_{j_1,j_2}\hat{E}_{j_1, j_2}}. - -(There shall be another page discussing weighted methods and variations) - - - -************* -Reference -************* - -.. [1] Wikipedia. (year). Cohen's kappa. https://en.wikipedia.org/wiki/Cohen%27s_kappa. -.. [2] Uebersax, J. (year). Kappa Coefficients: A Critical Appraisal. https://www.john-uebersax.com/stat/kappa.htm#procon. -.. [3] Brennan, R. L., & Prediger, D. J. (1981). Coefficient Kappa: Some Uses, Misuses, and Alternatives. Educational and Psychological Measurement, 41(3), 687-699. https://doi.org/10.1177/0013164481041003070 -.. [4] Cohen, J. (1960). A Coefficient of Agreement for Nominal Scales. Educational and Psychological Measurement, 20(1), 37-46. https://doi.org/10.1177/001316446002000104 -.. [5] McHugh M. L. (2012). Interrater reliability: the kappa statistic. Biochemia medica, 22(3), 276-282. https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3900052/ -.. [6] Sim, J., Wright, C. C. (2005). The Kappa Statistic in Reliability Studies: Use, Interpretation, and Sample Size Requirements, Physical Therapy, Volume 85, Issue 3, Pages 257-268, https://doi.org/10.1093/ptj/85.3.257 -.. [7] PSU. STAT504: Measure of Agreement: Kappa. https://online.stat.psu.edu/stat504/lesson/11/11.2/11.2.4 -.. [8] Basu, S., Banerjee, M., & Sen, A. (2000). Bayesian inference for kappa from single and multiple studies. Biometrics, 56(2), 577–582. https://doi.org/10.1111/j.0006-341x.2000.00577.x -.. [9] Hallgren K. A. (2012). Computing Inter-Rater Reliability for Observational Data: An Overview and Tutorial. Tutorials in quantitative methods for psychology, 8(1), 23–34. https://doi.org/10.20982/tqmp.08.1.p023 -.. [10] Landis, J. R., & Koch, G. G. (1977). The measurement of observer agreement for categorical data. Biometrics, 33(1), 159–174. \ No newline at end of file diff --git a/docs/statlab_kappa2.rst b/docs/statlab_kappa2.rst deleted file mode 100644 index 92f5292f..00000000 --- a/docs/statlab_kappa2.rst +++ /dev/null @@ -1,345 +0,0 @@ -.. - # Copyright (C) 2023-2024 Y Hsu - # - # This program is free software: you can redistribute it and/or modify - # it under the terms of the GNU General Public license as published by - # the Free software Foundation, either version 3 of the License, or - # any later version. - # - # This program is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - # GNU General Public License for more details - # - # You should have received a copy of the GNU General Public license - # along with this program. If not, see - -.. role:: red-b - -.. role:: red - -############# -StatLab/Reli/Fleiss's Kappa -############# - -:red-b:`Disclaimer:` -:red:`This page is provided only for studying and practicing. The author does not intend to promote or advocate any particular analysis method or software.` - -************* -Background -************* - -Fleiss's kappa (:math:`\kappa`) is a statistic used for describing inter-rater reliability of multiple independent raters -with categorical rating outcomes [1]_ [2]_. - -************* -Notation -************* - -Assume there are the same :math:`R+N_0` (:math:`\geq 2+N_0`) raters and each of :math:`n` samples were rated by :math:`R` randomly selected raters and were not rated by the rest of :math:`N_0` raters. -For :math:`J` categories rating, let :math:`Y_{r,i} \in \{v_0, v_1,v_2,\ldots, v_J \}` represent rating -from rater :math:`r=1,2,\ldots,R+N_0` for sample :math:`i = 1, \ldots, n`. -Let :math:`N_{ij}` represent the total number of raters gave rating :math:`(v_j)` to sample :math:`i`, where :math:`j \in \{0, 1,\ldots,J\}`. -The value :math:`v_0` represent raters did not rate the sample :math:`i` and :math:`N_{i0}=N_0` is a fixed number for all :math:`i`. -Therefore, :math:`v_0` will not be included in the discussion below. - -.. list-table:: Count of Ratings - :widths: 10 10 10 10 10 10 - :header-rows: 1 - :name: tbl_count1 - - * - - - :math:`v_1` - - :math:`v_2` - - :math:`\ldots` - - :math:`v_J` - - Row Total - * - **Sample:** 1 - - :math:`N_{11}` - - :math:`N_{12}` - - :math:`\ldots` - - :math:`N_{1J}` - - :math:`R` - * - **Sample:** 2 - - :math:`N_{21}` - - :math:`N_{22}` - - :math:`\ldots` - - :math:`N_{2J}` - - :math:`R` - * - **Sample:** 3 - - :math:`N_{31}` - - :math:`N_{32}` - - :math:`\ldots` - - :math:`N_{3J}` - - :math:`R` - * - :math:`\vdots` - - :math:`\vdots` - - :math:`\vdots` - - :math:`\ddots` - - :math:`\vdots` - - :math:`\vdots` - * - **Sample:** :math:`n` - - :math:`N_{n1}` - - :math:`N_{n2}` - - :math:`\ldots` - - :math:`N_{nJ}` - - :math:`R` - * - **Column total** - - :math:`N_{\bullet 1}` - - :math:`N_{\bullet 2}` - - :math:`\ldots` - - :math:`N_{\bullet J}` - - :math:`nR` - -The observed averaged agreement is calculated as - -.. math:: - :label: eq_obs1 - - \bar{p}_O = \frac{1}{n} \sum_{i=1}^n p_{O,i}, - -where :math:`p_{O,i} = \frac{1}{R(R-1)} \left(\sum_{j=1}^J N_{ij}(N_{ij}-1)\right)= \frac{1}{R(R-1)} \left(\sum_{j=1}^J N_{ij}^2 - R\right)`. - -The expected agreement is calculated as - -.. math:: - :label: eq_exp1 - - \bar{p}_E = \sum_{j=1}^J p_{E,j}^2, - -where :math:`p_{E,j} = \frac{N_{\bullet j}}{nR}`. - -The Fleiss's :math:`\kappa` statistic is calculated from :eq:`eq_obs1` and :eq:`eq_exp1` as - -.. math:: - :label: eq_kappa1 - - \kappa = \frac{\bar{p}_O - \bar{p}_E}{1-\bar{p}_E}. - -************* -Example - Group-1 -************* - -.. list-table:: Fleiss's :math:`\kappa = 1.0` - :widths: 10 10 10 10 10 - :header-rows: 1 - - * - - - :math:`v_1` - - :math:`v_2` - - :math:`v_3` - - :math:`v_4` - * - **Sample 1** - - 12 - - 0 - - 0 - - 0 - * - **Sample 2** - - 0 - - 12 - - 0 - - 0 - * - **Sample 3** - - 0 - - 0 - - 12 - - 0 - * - **Sample 4** - - 0 - - 0 - - 12 - - 0 - * - **Sample 5** - - 0 - - 0 - - 0 - - 12 - * - **Column Total** - - 12 - - 12 - - 24 - - 12 - - -.. list-table:: Fleiss's :math:`\kappa` = -0.0909090909090909 - :widths: 10 10 10 10 10 - :header-rows: 1 - - * - - - :math:`v_1` - - :math:`v_2` - - :math:`v_3` - - :math:`v_4` - * - **Sample 1** - - 3 - - 3 - - 3 - - 3 - * - **Sample 2** - - 3 - - 3 - - 3 - - 3 - * - **Sample 3** - - 3 - - 3 - - 3 - - 3 - * - **Sample 4** - - 3 - - 3 - - 3 - - 3 - * - **Sample 5** - - 3 - - 3 - - 3 - - 3 - * - **Column Total** - - 15 - - 15 - - 15 - - 15 - -************* -How-to -************* - -To use both ``statsmodels.stats.inter_rater`` and ``mtbp3.statlab``: - -.. testcode:: - - import statsmodels.stats.inter_rater as ir - from mtbp3.statlab import kappa - - r1 = ['NA'] * 20 + ['B'] * 50 + ['A'] * 30 - r2 = ['A'] * 20 + ['NA'] * 20 + ['B'] * 60 - r3 = ['A'] * 40 + ['NA'] * 20 + ['B'] * 30 + ['C'] * 10 - r4 = ['B'] * 60 + ['NA'] * 20 + ['C'] * 10 + ['A'] * 10 - r5 = ['C'] * 60 + ['A'] * 10 + ['B'] * 10 + ['NA'] * 20 - data = [r1, r2, r3, r4, r5] - kappa = KappaCalculator(data, stringna='NA') - - print("Fleiss's kappa (stasmodels.stats.inter_rater): "+str(ir.fleiss_kappa(kappa.y_count))) - print("Fleiss's kappa (mtbp3.statlab): "+str(kappa.fleiss_kappa)) - print("Number of raters per sample: "+str(kappa.n_rater)) - print("Number of rating categories: "+str(kappa.n_category)) - print("Number of sample: "+str(kappa.y_count.shape[0])) - -Output: - -.. testoutput:: - - Fleiss's kappa (stasmodels.stats.inter_rater): -0.14989733059548255 - Fleiss's kappa (mtbp3.statlab): -0.14989733059548255 - Number of raters per sample: 4.0 - Number of rating categories: 3 - Number of sample: 100 - -************* -Lab Exercise -************* - -1. Find Bootstrap CI of Fleiss's kappa. (see the function of Cohen's kappa CI) - -************* -More Details -************* - -:eq:`eq_obs1` corresponds to the observed -probability of having agreement for a sample from two randomly selected raters estimated from :numref:`Tabel %s `. -:eq:`eq_exp1` corresponds to the expected -probability of having agreement for a sample from two randomly selected raters under the assumption of no agreement, -which corresponds to the assumption of :math:`(N_{i1},\ldots, N_{iJ}) \sim multi(R, (p_1,\ldots, p_J))` where :math:`R>4`. - - -Let :math:`S_{p2} = \sum_j p_j^2`, :math:`S_{p3} = \sum_j p_j^3`, and :math:`S_{p4} = \sum_j p_j^4`. -The equation :eq:`eq_kappa1` can be expressed as [2]_ :sup:`(Eq. 9)`, - -.. math:: - - \kappa = \frac{\sum_{i=1}^{n}\sum_{j=1}^J N_{ij}^2 - nR\left(1+(R-1) S_{p2} \right)}{nR(R-1)(1- S_{p2} )} - - -Note that Fleiss (1971) assumed large :math:`n` and fixed :math:`p_j` while deriving variance of kappa. -Please see the Fleiss (1971) for more discussions. -The variance of :math:`\kappa` under the assumption of no agreement beyond chance can be approximated as: - -.. math:: - :label: eq_kappa2_vk - - var(\kappa) = c(n,R,\{p_j\}) var\left(\sum_{j=1}^J N_{1j}^2 \right), - -where - -.. math:: - - c(n,R,\{p_j\}) = n^{-1}\left(R(R-1)\left(1-S_{p2}\right)\right)^{-2}, - -and - -.. math:: - :label: eq_kappa2_vn2 - - var\left(\sum_{j} N_{ij}^2 \right) - =& E\left( \left(\sum_{j} N_{ij}^2\right)^2\right) - \left(E\left(\sum_{j} N_{ij}^2\right)\right)^2 \\ - =& E\left(\sum_{j} N_{ij}^4\right) + E\left(\sum_j\sum_{k \neq j} N_{ij}^2 N_{ik}^2 \right) - \left(E\left(\sum_{j} N_{ij}^2\right)\right)^2. - -To calculate :eq:`eq_kappa2_vn2`, -we can use the MGF, :math:`\left(\sum_{j}p_je^{t_j}\right)^R`, to derive -:math:`E\left(N_{ij}^2\right) = Rp_j + R(R-1)p_j^2`, and -:math:`E\left(N_{ij}^3\right) = Rp_j + 3R(R-1)p_j^2 + R(R-1)(R-2)p_j^3`. - -The first element of :eq:`eq_kappa2_vn2` can be calculated as [2]_ :sup:`(Eq. 12)` - -.. math:: - :label: eq_kappa2_vn3 - - E\left(\sum_{j} N_{ij}^4\right) - = R + 7R(R-1)S_{p2} + 6R(R-1)(R-2)S_{p3} + R(R-1)(R-2)(R-3)S_{p4} - -The third element of :eq:`eq_kappa2_vn2` can be calculated as [2]_ :sup:`(Eq. 14)` - -.. math:: - :label: eq_kappa2_vn4 - - \left(E\left(\sum_{j} N_{ij}^2\right)\right)^2 - =& R^2\left(1 + (R-1)S_{p2} \right)^2 \\ - =& R^2 + R^2(R-1)\left(2 S_{p2} + (R-1)S_{p2}^2\right) - -The second element of :eq:`eq_kappa2_vn2` can be calculated, using -:math:`E\left( N_{ij}^2 N_{ik}^2 \right) = R(R-1)p_j(p_k+(R-2)p_k^2) + R(R-1)(R-2)p_j^2(p_k+(R-3)p_k^2)`, as - -.. math:: - :label: eq_kappa2_vn5 - - E\left( \sum_j\sum_{k \neq j} N_{ij}^2 N_{ik}^2 \right) - =& R(R-1) + R(R-1)(2R-5)S_{p2} - - 2R(R-1)(R-2)S_{p3} \\ - &- R(R-1)(R-2)(R-3)S_{p4} + R(R-1)(R-2)(R-3) S_{p2}^2 - -Combining :eq:`eq_kappa2_vn3`, :eq:`eq_kappa2_vn4`, and :eq:`eq_kappa2_vn5`, -:eq:`eq_kappa2_vn2` can be calculated as [2]_ :sup:`(Eq. 15)` - -.. math:: - - var\left(\sum_{j} N_{ij}^2 \right) - = 2R(R-1)\left(S_{p2} - (2R-3)S_{p2}^2 + 2(R-2)S_{p3}\right). - -Let :math:`s^2` be the estimated variance of :math:`\kappa` using :eq:`eq_kappa2_vk`. -Under the hypothesis of no agreement beyond chances, the limit distribution :math:`\kappa/s` would be a standard normal distribution. -The value of :math:`\kappa/s` then could be used to describe if the overall agreement is greater then by chance alone [2]_. - -************* -Lab Exercise -************* - -2. Find :math:`Cov(N_{i1},N_{i2})` under no agreement assumption. - -************* -Reference -************* - -.. [1] Wikipedia. (year). Fleiss's kappa. https://en.wikipedia.org/wiki/Fleiss%27_kappa -.. [2] Fleiss, J. L. (1971). Measuring nominal scale agreement among many raters. Psychological Bulletin, 76(5), 378-382. https://doi.org/10.1037/h0031619 - diff --git a/mtbp3/statlab/__init__.py b/mtbp3/statlab/__init__.py deleted file mode 100644 index ebf38adf..00000000 --- a/mtbp3/statlab/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .kappa import * diff --git a/mtbp3/statlab/corr.py b/mtbp3/statlab/corr.py deleted file mode 100644 index f3131fb7..00000000 --- a/mtbp3/statlab/corr.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright (C) 2023-2024 Y Hsu -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public license as published by -# the Free software Foundation, either version 3 of the License, or -# any later version. -#j -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details -# -# You should have received a copy of the GNU General Public license -# along with this program. If not, see - -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt - -class CorrCalculator: - """ - A class for calculating correlation and plotting scatter plots. - - Parameters: - - y: A pandas DataFrame or a list - - If y is a pandas DataFrame, it must be a 2-dimensional DataFrame with at least 2 columns and 2 rows. - - If y is a list, it must be a list with at least 2 elements, where each element is a list of strings or numbers. - - remove_na: bool, optional (default=True) - - If True, remove rows with missing values (NA) from the input data. - - If False, raise a ValueError if the input data contains missing values (NA). - - Methods: - - calculate_kendall_tau(): - - Calculates the Kendall's tau correlation coefficient between the first two columns of the input data. - - Returns the Kendall's tau coefficient as a float. - - - plot_y_list(loc=[0,1], axis_label=['x','y']): - - Plots a scatter plot of the input data. - - loc: list, optional (default=[0,1]) - - The indices of the columns to be plotted on the x and y axes. - - axis_label: list, optional (default=['x','y']) - - The labels for the x and y axes of the scatter plot. - """ - - def __init__(self, y, remove_na=True): - - assert isinstance(y, (pd.DataFrame, list)), "y must be either a pandas DataFrame or a list" - if isinstance(y, pd.DataFrame): - assert y.ndim == 2, "y must be a 2-dimensional DataFrame" - assert y.shape[0] >= 2 and y.shape[1] >= 2, "y must be a pd.DataFrame with at least 2 columns and 2 rows" - if remove_na == True: - self.y_df = y.dropna() - else: - if y.isna().any().any(): - raise ValueError("Input data contains missing values (NA)") - self.y_shape0, self.y_shape1 = self.y_df.shape - self.y_list = self.y_df.values.tolist() - else: - assert isinstance(y, list) and len(y) >= 2, "y must be a list with at least 2 elements" - assert all(isinstance(x, list) for x in y), "all elements of y must be lists" - assert all(isinstance(x, (str, int)) for sublist in y for x in sublist if x is not None), "all elements of y must be strings or numbers" - assert all(len(x) == len(y[0]) for x in y), "all sublists in y must have the same length" - self.y_shape0 = len(y[0]) - self.y_shape1 = len(y) - self.y_list = y - self.y_df = pd.DataFrame(self.y_list).T.dropna() - - return - - @staticmethod - def __g(x): - result = [np.sign(sublist[0] - sublist[1]) for sublist in x] - return result - - def calculate_kendall_tau(self): - """ - Calculates the Kendall's tau correlation coefficient between the first two columns of the input data. - - Returns: - - tau: float - - The Kendall's tau correlation coefficient. - """ - tau1 = np.sign(np.repeat(self.y_list[0], self.y_shape0)-np.tile(self.y_list[0], self.y_shape0)) - tau2 = np.sign(np.repeat(self.y_list[1], self.y_shape0)-np.tile(self.y_list[1], self.y_shape0)) - tau = np.sum(np.multiply(tau1,tau2))/np.sqrt(np.multiply(np.sum(np.abs(tau1)),np.sum(np.abs(tau2)))) - return tau - - - def plot_y_list(self, loc=[0,1], axis_label=['x','y']): - """ - Plots a scatter plot of the input data. - - Parameters: - - loc: list, optional (default=[0,1]) - - The indices of the columns to be plotted on the x and y axes. - - axis_label: list, optional (default=['x','y']) - - The labels for the x and y axes of the scatter plot. - """ - plt.scatter(self.y_list[loc[0]], self.y_list[loc[1]]) - plt.xlabel(axis_label[0]) - plt.ylabel(axis_label[1]) - plt.title('Scatter Plot') - plt.show() - -if __name__ == "__main__": - - pass diff --git a/mtbp3/statlab/kappa.py b/mtbp3/statlab/kappa.py deleted file mode 100644 index a156b6ea..00000000 --- a/mtbp3/statlab/kappa.py +++ /dev/null @@ -1,330 +0,0 @@ -# Copyright (C) 2023-2024 Y Hsu -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public license as published by -# the Free software Foundation, either version 3 of the License, or -# any later version. -#j -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details -# -# You should have received a copy of the GNU General Public license -# along with this program. If not, see - -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt -from sklearn.utils import resample -import os -import seaborn as sns -import random - - -class KappaCalculator: - """ - A class for calculating Cohen's kappa and Fleiss' kappa. - - Parameters: - - y: The input data. It can be either a pandas DataFrame or a list. - - infmt: The format of the input data. Allowed values are 'sample_list', 'sample_df', 'count_sq_df', and 'count_df'. - - stringna: The string representation of missing values. - - Methods: - - bootstrap_cohen_ci(n_iterations, confidence_level, outfmt, out_digits): Calculates the bootstrap confidence interval for Cohen's kappa. - - """ - - def __init__(self, y, infmt='sample_list', stringna="stringna"): - """ - Initializes the KappaCalculator object. - - Parameters: - - y: The input data. It can be either a pandas DataFrame or a list. - - infmt: The format of the input data. Allowed values are 'sample_list', 'sample_df', 'count_sq_df', and 'count_df'. - - stringna: The string representation of missing values. - - Raises: - - ValueError: If the value of infmt is invalid. - - AssertionError: If the input data does not meet the required conditions. - - """ - - assert isinstance(y, (pd.DataFrame, list)), "y must be either a pandas DataFrame or a list" - if isinstance(y, pd.DataFrame): - assert y.ndim == 2, "y must be a 2-dimensional DataFrame" - assert y.shape[0] >= 2 and y.shape[1] >= 2, "y must be a pd.DataFrame with at least 2 columns and 2 rows" - else: - assert isinstance(y, list) and len(y) >= 2, "y must be a list with at least 2 elements" - assert all(isinstance(x, list) for x in y), "all elements of y must be lists" - assert all(isinstance(x, (str, int)) for sublist in y for x in sublist if x is not None), "all elements of y must be strings or numbers" - assert all(len(x) == len(y[0]) for x in y), "all sublists in y must have the same length" - - if infmt not in ['sample_list', 'sample_df', 'count_sq_df', 'count_df']: - raise ValueError("Invalid value for infmt. Allowed values are 'sample_list', 'sample_df', 'count_sq_df' and 'count_df'.") - - if infmt == 'sample_list' or infmt == 'sample_df': - if infmt == 'sample_list': - self.y_list = self.__convert_2dlist_to_string(y, stringna=stringna) - self.y_df = pd.DataFrame(self.y_list).T - else: - self.y_df = y.replace({np.nan: stringna, None: stringna}) - self.y_df = self.y_df.applymap(lambda x: str(x) if isinstance(x, (int, float)) else x) - self.y_list = self.y_df.values.tolist() - - self.y_count = self.y_df.apply(pd.Series.value_counts, axis=1).fillna(0) - if stringna in self.y_count.columns: - column_values = self.y_count[stringna].unique() - assert len(column_values) == 1, f"Total number in value '{stringna}' must be the same for all sample" - self.y_count.drop(columns=stringna, inplace=True) - tmp_row_sum = self.y_count.sum(axis=1) - assert tmp_row_sum.eq(tmp_row_sum[0]).all(), "Total number of raters per sample must be equal" - self.category = self.y_count.columns - self.n_category = len(self.category) - self.n_rater = tmp_row_sum[0] - - if self.n_rater == 2: - self.y_count_sq = pd.crosstab(self.y_list[0], self.y_list[1], margins = False, dropna=False) - i = self.y_count_sq.index.union(self.y_count_sq.columns, sort=True) - self.y_count_sq.reindex(index=i, columns=i, fill_value=0) - else: - self.y_count_sq = None - - elif infmt == 'count_sq_df': - assert y.shape[0] == y.shape[1], "y must be a square DataFrame" - self.y_count_sq = y - self.category = y.columns - self.n_category = len(self.category) - self.n_rater = 2 - tmp_count = self.y_count_sq.unstack().reset_index(name='count') - self.y_df = tmp_count.loc[np.repeat(tmp_count.index.values, tmp_count['count'])] - self.y_df.drop(columns='count', inplace=True) - self.y_list = self.y_df.values.tolist() - self.y_count = self.y_df.apply(pd.Series.value_counts, axis=1).fillna(0) - - elif infmt == 'count_df': - self.y_count = y - if stringna in self.y_count.columns: - column_values = self.y_count[stringna].unique() - assert len(column_values) == 1, f"All values in column '{stringna}' must be the same" - self.y_count.drop(columns=stringna, inplace=True) - tmp_row_sum = self.y_count.sum(axis=1) - assert tmp_row_sum.eq(tmp_row_sum[0]).all(), "Row sums of y must be equal" - self.category = self.y_count.columns - self.n_category = len(self.category) - self.n_rater = tmp_row_sum[0] - self.y_count_sq= None - self.y_list = None - self.y_df = None - - else: - self.y_list = None - self.y_df = None - self.y_count= None - self.y_count_sq= None - self.category = None - self.n_rater = None - self.n_category = None - return - - - if self.n_rater == 2: - if self.y_list is not None: - self.cohen_kappa = self.__calculate_cohen_kappa(self.y_list[0],self.y_list[1]) - else: - self.cohen_kappa = None - else: - self.cohen_kappa = None - - if self.n_rater >= 2: - if self.y_count is not None: - self.fleiss_kappa = self.__calculate_fleiss_kappa(self.y_count) - else: - self.fleiss_kappa = None - else: - self.fleiss_kappa = None - - return - - @staticmethod - def __convert_2dlist_to_string(y=[], stringna=""): - """ - Converts a 2-dimensional list to a string representation. - - Parameters: - - y: The input list. - - stringna: The string representation of missing values. - - Returns: - - The converted list. - - """ - for i in range(len(y)): - if any(isinstance(x, (int)) for x in y[i]): - y[i] = [str(x) if x is not None else stringna for x in y[i]] - return y - - @staticmethod - def __calculate_cohen_kappa(y1, y2): - """ - Calculates Cohen's kappa. - - Parameters: - - y1: The first rater's ratings. - - y2: The second rater's ratings. - - Returns: - - The calculated Cohen's kappa value. - - """ - total_pairs = len(y1) - observed_agreement = sum(1 for i in range(total_pairs) if y1[i] == y2[i]) / total_pairs - unique_labels = set(y1 + y2) - expected_agreement = sum((y1.count(label) / total_pairs) * (y2.count(label) / total_pairs) for label in unique_labels) - return (observed_agreement - expected_agreement) / (1 - expected_agreement) - - @staticmethod - def __calculate_fleiss_kappa(y): - """ - Calculates Fleiss' kappa. - - Parameters: - - y: The count matrix. - - Returns: - - The calculated Fleiss' kappa value. - - """ - nR = y.values.sum() - p = y.values.sum(axis = 0)/nR - Pbar_E = (p ** 2).sum() - R = y.values.sum(axis = 1)[0] - Pbar_O = (((y ** 2).sum(axis=1) - R) / (R * (R - 1))).mean() - - return (Pbar_O - Pbar_E) / (1 - Pbar_E) - - def bootstrap_cohen_ci(self, n_iterations=1000, confidence_level=0.95, outfmt='string', out_digits=6): - """ - Calculates the bootstrap confidence interval for Cohen's kappa. - - Parameters: - - n_iterations: The number of bootstrap iterations. - - confidence_level: The desired confidence level. - - outfmt: The output format. Allowed values are 'string' and 'list'. - - out_digits: The number of digits to round the output values. - - Returns: - - If outfmt is 'string', returns a string representation of the result. - - If outfmt is 'list', returns a list containing the result values. - - """ - assert isinstance(n_iterations, int) and n_iterations > 1, "n_iterations must be an integer greater than 1" - assert isinstance(confidence_level, (float)) and 0 < confidence_level < 1, "confidence_level must be a number between 0 and 1" - - if self.n_rater != 2: - return [] - - y1 = self.y_list[0] - y2 = self.y_list[1] - kappa_values = [] - idx = range(len(y1)) - for _ in range(n_iterations): - idxr = resample(idx) - y1r = [y1[i] for i in idxr] - y2r = [y2[i] for i in idxr] - - kappa = self.__calculate_cohen_kappa(y1r, y2r) - kappa_values.append(kappa) - - lower_percentile = (1 - confidence_level) / 2 - upper_percentile = 1 - lower_percentile - lower_bound = np.percentile(kappa_values, lower_percentile * 100) - upper_bound = np.percentile(kappa_values, upper_percentile * 100) - if outfmt=='string': - return "Cohen's kappa: {:.{}f}".format(self.cohen_kappa, out_digits) + "\nConfidence Interval ({}%): [{:.{}f}, {:.{}f}]".format(confidence_level * 100, lower_bound, out_digits, upper_bound, out_digits) - else: - return [self.cohen_kappa, n_iterations, confidence_level, lower_bound, upper_bound] - - def create_bubble_plot(self, out_path="", axis_label=[], max_size_ratio=0, hist=False, reverse_y=False): - """ - Creates a bubble plot based on the y_count_sq matrix. - - Parameters: - - out_path (str): The output path to save the plot. If not provided, the plot will be displayed. - - title (str): The title of the plot. If not provided, the default title is 'Bubble Plot'. - - axis_label (list): A list of two strings representing the labels for the x-axis and y-axis. - If not provided, the default labels are ['Rater 1', 'Rater 2']. - - max_size_ratio (int): The maximum size ratio for the bubbles. The size of the bubbles is determined by the values in the y_count_sq matrix. - The maximum size of the bubbles will be max_size_ratio times the maximum value in the matrix. - If not provided, the default value is 100. - - Returns: - - None - - Raises: - - None - - """ - if self.n_rater == 2 and self.y_count_sq is not None and self.y_count_sq.shape[0] == self.y_count_sq.shape[1] and self.y_count_sq.shape[0] > 0: - categories = self.y_count_sq.columns - n_categories = len(categories) - - r1 = [] - r2 = [] - sizes = [] - for i1, c1 in enumerate(categories): - for i2, c2 in enumerate(categories): - r1.append(c1) - r2.append(c2) - sizes.append(self.y_count_sq.iloc[i1, i2]) - df0 = pd.DataFrame({'r1': r1, 'r2': r2, 'sizes': sizes}) - df0['r1'] = pd.Categorical(df0['r1']) - df0['r2'] = pd.Categorical(df0['r2']) - df0['agree'] = np.where(df0['r1'] == df0['r2'], 'agree', 'disagree') - max_size_ratio = max_size_ratio if max_size_ratio >= 1 else max(1,int((6000/max(sizes)) / n_categories)) - if hist: - sns.jointplot( - data=df0, x="r1", y="r2", kind="scatter", - height=5, ratio=3, marginal_ticks=True, - marginal_kws={"hue": df0['agree'], "multiple": "stack", "weights": sizes, "shrink":.5, "legend": False}, - joint_kws={"hue": df0['agree'], "size": sizes, "legend": False, "sizes":(min(sizes)*max_size_ratio, max(sizes)*max_size_ratio)} - ) - if not reverse_y: - tmp1 = plt.ylim() - plt.ylim(tmp1[1], tmp1[0]) - else: - sns.scatterplot(data=df0, x="r1", y="r2", size="sizes", hue="agree", sizes=(min(sizes)*max_size_ratio, max(sizes)*max_size_ratio), legend=False) - tmp1 = plt.xlim() - tmp1d = ((tmp1[1] - tmp1[0])/n_categories) - plt.xlim(tmp1[0] - tmp1d, tmp1[1] + tmp1d) - plt.ylim(tmp1[0] - tmp1d, tmp1[1] + tmp1d) - if reverse_y: - tmp1 = plt.ylim() - plt.ylim(tmp1[1], tmp1[0]) - - for i in range(len(df0)): - plt.text(df0['r1'][i], df0['r2'][i], df0['sizes'][i], ha='center', va='center') - - if not axis_label: - axis_label = ['Rater 1', 'Rater 2'] - plt.xlabel(axis_label[0]) - plt.ylabel(axis_label[1]) - - plt.tight_layout() - if out_path: - try: - if os.path.isdir(out_path): - plt.savefig(os.path.join(out_path, "bubble_plot.svg")) - else: - plt.savefig(out_path) - except Exception as e: - print("Error saving the figure:", str(e)) - else: - plt.show() - else: - print("Cannot create bubble plot. y_count_sq is not a square non-empty matrix.") - -if __name__ == "__main__": - - pass diff --git a/tests/test_statlab_corr.py b/tests/test_statlab_corr.py deleted file mode 100644 index 0e4683b7..00000000 --- a/tests/test_statlab_corr.py +++ /dev/null @@ -1,30 +0,0 @@ -import unittest -import pandas as pd -import numpy as np -from mtbp3.statlab.corr import CorrCalculator -import matplotlib.pyplot as plt - -class TestCorrCalculator(unittest.TestCase): - - def setUp(self): - self.y_df = pd.DataFrame({'x': [1, 2, 3, 4, 5], 'y': [2, 4, 6, 8, 10]}) - self.y_list = [[1, 2, 3, 4, 5], [2, 4, 6, 8, 10]] - self.corr_calculator_df = CorrCalculator(self.y_df) - self.corr_calculator_list = CorrCalculator(self.y_list) - - def test_calculate_kendall_tau_with_dataframe(self): - expected_tau = 1.0 - tau = self.corr_calculator_df.calculate_kendall_tau() - self.assertEqual(tau, expected_tau) - - def test_calculate_kendall_tau_with_list(self): - expected_tau = 1.0 - tau = self.corr_calculator_list.calculate_kendall_tau() - self.assertEqual(tau, expected_tau) - - def test_plot_y_list(self): - self.corr_calculator_df.plot_y_list() - # No assertion, just checking if the plot is displayed correctly - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/tests/test_statlab_kappa.py b/tests/test_statlab_kappa.py deleted file mode 100644 index d808eda9..00000000 --- a/tests/test_statlab_kappa.py +++ /dev/null @@ -1,40 +0,0 @@ -import unittest -from mtbp3.statlab.kappa import KappaCalculator -import statsmodels.stats.inter_rater as ir - -class TestKappaCalculator(unittest.TestCase): - - def setUp(self): - y1 = ['B'] * 70 + ['A'] * 30 - y2 = ['A'] * 70 + ['B'] * 30 - y3 = ['A'] * 50 + ['B'] * 30 + ['C'] * 20 - y4 = ['B'] * 40 + ['C'] * 40 + ['A'] * 20 - y5 = ['C'] * 60 + ['A'] * 20 + ['B'] * 20 - data = [y1, y2, y3, y4, y5] - self.c1 = KappaCalculator([y1, y2]) - self.c2 = KappaCalculator(data) - - def test_cohen(self): - gt0 = ir.cohens_kappa(self.c1.y_count_sq) - self.assertAlmostEqual(gt0.kappa, self.c1.cohen_kappa, places=6) - self.assertIsInstance(self.c1.cohen_kappa, float) - self.assertGreaterEqual(self.c1.cohen_kappa, -1) - self.assertLessEqual(self.c1.cohen_kappa, 1) - - def test_bootstrap_cohen_ci(self): - result = self.c1.bootstrap_cohen_ci(n_iterations=1000, confidence_level=0.95, out_digits=6) - self.assertIsInstance(result, str) - self.assertIn("Cohen's kappa:", result) - self.assertIn("Confidence Interval", result) - - def test_fleiss_kappa(self): - gt1 = ir.fleiss_kappa(self.c1.y_count) - gt2 = ir.fleiss_kappa(self.c2.y_count) - self.assertAlmostEqual(gt1, self.c1.fleiss_kappa, places=6) - self.assertAlmostEqual(gt2, self.c2.fleiss_kappa, places=6) - self.assertIsInstance(gt2, float) - self.assertGreaterEqual(gt2, -1) - self.assertLessEqual(gt2, 1) - -if __name__ == "__main__": - unittest.main() \ No newline at end of file