aim-cu 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aim_cu-0.2.0/LICENSE +121 -0
- aim_cu-0.2.0/PKG-INFO +120 -0
- aim_cu-0.2.0/aim_cu/README.rst +97 -0
- aim_cu-0.2.0/aim_cu/SECURITY.md +38 -0
- aim_cu-0.2.0/aim_cu/__init__.py +27 -0
- aim_cu-0.2.0/aim_cu/aim_cu.py +623 -0
- aim_cu-0.2.0/aim_cu/config.toml +21 -0
- aim_cu-0.2.0/aim_cu.egg-info/PKG-INFO +120 -0
- aim_cu-0.2.0/aim_cu.egg-info/SOURCES.txt +13 -0
- aim_cu-0.2.0/aim_cu.egg-info/dependency_links.txt +1 -0
- aim_cu-0.2.0/aim_cu.egg-info/requires.txt +7 -0
- aim_cu-0.2.0/aim_cu.egg-info/top_level.txt +1 -0
- aim_cu-0.2.0/setup.cfg +4 -0
- aim_cu-0.2.0/setup.py +31 -0
- aim_cu-0.2.0/test/test_aim_cu.py +90 -0
aim_cu-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Creative Commons Legal Code
|
|
2
|
+
|
|
3
|
+
CC0 1.0 Universal
|
|
4
|
+
|
|
5
|
+
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
|
6
|
+
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
|
7
|
+
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
|
8
|
+
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
|
9
|
+
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
|
10
|
+
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
|
11
|
+
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
|
12
|
+
HEREUNDER.
|
|
13
|
+
|
|
14
|
+
Statement of Purpose
|
|
15
|
+
|
|
16
|
+
The laws of most jurisdictions throughout the world automatically confer
|
|
17
|
+
exclusive Copyright and Related Rights (defined below) upon the creator
|
|
18
|
+
and subsequent owner(s) (each and all, an "owner") of an original work of
|
|
19
|
+
authorship and/or a database (each, a "Work").
|
|
20
|
+
|
|
21
|
+
Certain owners wish to permanently relinquish those rights to a Work for
|
|
22
|
+
the purpose of contributing to a commons of creative, cultural and
|
|
23
|
+
scientific works ("Commons") that the public can reliably and without fear
|
|
24
|
+
of later claims of infringement build upon, modify, incorporate in other
|
|
25
|
+
works, reuse and redistribute as freely as possible in any form whatsoever
|
|
26
|
+
and for any purposes, including without limitation commercial purposes.
|
|
27
|
+
These owners may contribute to the Commons to promote the ideal of a free
|
|
28
|
+
culture and the further production of creative, cultural and scientific
|
|
29
|
+
works, or to gain reputation or greater distribution for their Work in
|
|
30
|
+
part through the use and efforts of others.
|
|
31
|
+
|
|
32
|
+
For these and/or other purposes and motivations, and without any
|
|
33
|
+
expectation of additional consideration or compensation, the person
|
|
34
|
+
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
|
35
|
+
is an owner of Copyright and Related Rights in the Work, voluntarily
|
|
36
|
+
elects to apply CC0 to the Work and publicly distribute the Work under its
|
|
37
|
+
terms, with knowledge of his or her Copyright and Related Rights in the
|
|
38
|
+
Work and the meaning and intended legal effect of CC0 on those rights.
|
|
39
|
+
|
|
40
|
+
1. Copyright and Related Rights. A Work made available under CC0 may be
|
|
41
|
+
protected by copyright and related or neighboring rights ("Copyright and
|
|
42
|
+
Related Rights"). Copyright and Related Rights include, but are not
|
|
43
|
+
limited to, the following:
|
|
44
|
+
|
|
45
|
+
i. the right to reproduce, adapt, distribute, perform, display,
|
|
46
|
+
communicate, and translate a Work;
|
|
47
|
+
ii. moral rights retained by the original author(s) and/or performer(s);
|
|
48
|
+
iii. publicity and privacy rights pertaining to a person's image or
|
|
49
|
+
likeness depicted in a Work;
|
|
50
|
+
iv. rights protecting against unfair competition in regards to a Work,
|
|
51
|
+
subject to the limitations in paragraph 4(a), below;
|
|
52
|
+
v. rights protecting the extraction, dissemination, use and reuse of data
|
|
53
|
+
in a Work;
|
|
54
|
+
vi. database rights (such as those arising under Directive 96/9/EC of the
|
|
55
|
+
European Parliament and of the Council of 11 March 1996 on the legal
|
|
56
|
+
protection of databases, and under any national implementation
|
|
57
|
+
thereof, including any amended or successor version of such
|
|
58
|
+
directive); and
|
|
59
|
+
vii. other similar, equivalent or corresponding rights throughout the
|
|
60
|
+
world based on applicable law or treaty, and any national
|
|
61
|
+
implementations thereof.
|
|
62
|
+
|
|
63
|
+
2. Waiver. To the greatest extent permitted by, but not in contravention
|
|
64
|
+
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
|
65
|
+
irrevocably and unconditionally waives, abandons, and surrenders all of
|
|
66
|
+
Affirmer's Copyright and Related Rights and associated claims and causes
|
|
67
|
+
of action, whether now known or unknown (including existing as well as
|
|
68
|
+
future claims and causes of action), in the Work (i) in all territories
|
|
69
|
+
worldwide, (ii) for the maximum duration provided by applicable law or
|
|
70
|
+
treaty (including future time extensions), (iii) in any current or future
|
|
71
|
+
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
|
72
|
+
including without limitation commercial, advertising or promotional
|
|
73
|
+
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
|
74
|
+
member of the public at large and to the detriment of Affirmer's heirs and
|
|
75
|
+
successors, fully intending that such Waiver shall not be subject to
|
|
76
|
+
revocation, rescission, cancellation, termination, or any other legal or
|
|
77
|
+
equitable action to disrupt the quiet enjoyment of the Work by the public
|
|
78
|
+
as contemplated by Affirmer's express Statement of Purpose.
|
|
79
|
+
|
|
80
|
+
3. Public License Fallback. Should any part of the Waiver for any reason
|
|
81
|
+
be judged legally invalid or ineffective under applicable law, then the
|
|
82
|
+
Waiver shall be preserved to the maximum extent permitted taking into
|
|
83
|
+
account Affirmer's express Statement of Purpose. In addition, to the
|
|
84
|
+
extent the Waiver is so judged Affirmer hereby grants to each affected
|
|
85
|
+
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
|
86
|
+
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
|
87
|
+
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
|
88
|
+
maximum duration provided by applicable law or treaty (including future
|
|
89
|
+
time extensions), (iii) in any current or future medium and for any number
|
|
90
|
+
of copies, and (iv) for any purpose whatsoever, including without
|
|
91
|
+
limitation commercial, advertising or promotional purposes (the
|
|
92
|
+
"License"). The License shall be deemed effective as of the date CC0 was
|
|
93
|
+
applied by Affirmer to the Work. Should any part of the License for any
|
|
94
|
+
reason be judged legally invalid or ineffective under applicable law, such
|
|
95
|
+
partial invalidity or ineffectiveness shall not invalidate the remainder
|
|
96
|
+
of the License, and in such case Affirmer hereby affirms that he or she
|
|
97
|
+
will not (i) exercise any of his or her remaining Copyright and Related
|
|
98
|
+
Rights in the Work or (ii) assert any associated claims and causes of
|
|
99
|
+
action with respect to the Work, in either case contrary to Affirmer's
|
|
100
|
+
express Statement of Purpose.
|
|
101
|
+
|
|
102
|
+
4. Limitations and Disclaimers.
|
|
103
|
+
|
|
104
|
+
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
|
105
|
+
surrendered, licensed or otherwise affected by this document.
|
|
106
|
+
b. Affirmer offers the Work as-is and makes no representations or
|
|
107
|
+
warranties of any kind concerning the Work, express, implied,
|
|
108
|
+
statutory or otherwise, including without limitation warranties of
|
|
109
|
+
title, merchantability, fitness for a particular purpose, non
|
|
110
|
+
infringement, or the absence of latent or other defects, accuracy, or
|
|
111
|
+
the present or absence of errors, whether or not discoverable, all to
|
|
112
|
+
the greatest extent permissible under applicable law.
|
|
113
|
+
c. Affirmer disclaims responsibility for clearing rights of other persons
|
|
114
|
+
that may apply to the Work or any use thereof, including without
|
|
115
|
+
limitation any person's Copyright and Related Rights in the Work.
|
|
116
|
+
Further, Affirmer disclaims responsibility for obtaining any necessary
|
|
117
|
+
consents, permissions or other rights required for any use of the
|
|
118
|
+
Work.
|
|
119
|
+
d. Affirmer understands and acknowledges that Creative Commons is not a
|
|
120
|
+
party to this document and has no duty or obligation with respect to
|
|
121
|
+
this CC0 or use of the Work.
|
aim_cu-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aim-cu
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: AIM-CU: A CUSUM-based tool for AI Monitoring
|
|
5
|
+
Home-page: https://github.com/DIDSR/AIM-CU
|
|
6
|
+
Project-URL: User Inteface, https://huggingface.co/spaces/didsr/AIM-CU
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/x-rst
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: numpy>=1.23
|
|
11
|
+
Requires-Dist: pandas>=1.5
|
|
12
|
+
Requires-Dist: matplotlib>=3.6
|
|
13
|
+
Requires-Dist: rpy2>=3.5
|
|
14
|
+
Requires-Dist: tomli; python_version < "3.11"
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
Dynamic: project-url
|
|
20
|
+
Dynamic: requires-dist
|
|
21
|
+
Dynamic: requires-python
|
|
22
|
+
Dynamic: summary
|
|
23
|
+
|
|
24
|
+
AIM-CU: A CUSUM-based tool for AI Monitoring
|
|
25
|
+
============================================
|
|
26
|
+
Monitoring a clinically deployed AI device to detect performance drift is an essential step to ensure the safety and effectiveness of AI.
|
|
27
|
+
|
|
28
|
+
AIM-CU is a statistical tool for AI monitoring based on a cumulative sum (AIM-CU) approach.
|
|
29
|
+
|
|
30
|
+
AIM-CU computes:
|
|
31
|
+
|
|
32
|
+
* The parameter choices for change-point detection based on an acceptable false alarm rate
|
|
33
|
+
* Detection delay estimates for a given displacement of the performance metric from the target for those parameter choices.
|
|
34
|
+
|
|
35
|
+
System setup
|
|
36
|
+
------------
|
|
37
|
+
Make sure R is installed in the system. There is no specific version that this relies on. Here we have used the version 4.1.2 (2021-11-01).
|
|
38
|
+
Instructions for Linux (the below setup is only performed in Linux):
|
|
39
|
+
|
|
40
|
+
.. code-block:: shell
|
|
41
|
+
|
|
42
|
+
wget -qO- https://cloud.r-project.org/bin/linux/ubuntu/marutter_pubkey.asc | tee -a /etc/apt/trusted.gpg.d/cran_ubuntu_key.asc
|
|
43
|
+
add-apt-repository "deb https://cloud.r-project.org/bin/linux/ubuntu $(lsb_release -cs)-cran40/"
|
|
44
|
+
apt-get install -y --no-install-recommends r-base r-base-dev
|
|
45
|
+
|
|
46
|
+
# setup R configs
|
|
47
|
+
echo "r <- getOption('repos'); r['CRAN'] <- 'http://cran.us.r-project.org'; options(repos = r);" > ~/.Rprofile
|
|
48
|
+
Rscript -e "install.packages('ggplot2')"
|
|
49
|
+
Rscript -e "install.packages('hexbin')"
|
|
50
|
+
Rscript -e "install.packages('lazyeval')"
|
|
51
|
+
Rscript -e "install.packages('cusumcharter')"
|
|
52
|
+
Rscript -e "install.packages('RcppCNPy')"
|
|
53
|
+
Rscript -e "install.packages('spc')"
|
|
54
|
+
|
|
55
|
+
Code execution
|
|
56
|
+
--------------
|
|
57
|
+
Clone AIM-CU repository.
|
|
58
|
+
|
|
59
|
+
.. code-block:: shell
|
|
60
|
+
|
|
61
|
+
git clone https://github.com/DIDSR/AIM-CU.git
|
|
62
|
+
|
|
63
|
+
Run the following commands to install required dependencies (Python = 3.10 is used).
|
|
64
|
+
|
|
65
|
+
.. code-block:: shell
|
|
66
|
+
|
|
67
|
+
apt-get -y install python3
|
|
68
|
+
apt-get -y install pip
|
|
69
|
+
cd AIM-CU
|
|
70
|
+
pip install -r requirements.txt
|
|
71
|
+
|
|
72
|
+
Run AIM-CU.
|
|
73
|
+
|
|
74
|
+
.. code-block:: shell
|
|
75
|
+
|
|
76
|
+
cd src/package
|
|
77
|
+
python3 app.py
|
|
78
|
+
|
|
79
|
+
Open the URL http://0.0.0.0:7860 that is running the AIM-CU locally.
|
|
80
|
+
|
|
81
|
+
Example code execution
|
|
82
|
+
----------------------
|
|
83
|
+
Example code can be run through Jupyter Notebook. Do this by entering the ``jupyter notebook`` command from the ``/src/package/`` directory. The tool is designed to be used through a GUI, not from the console.
|
|
84
|
+
|
|
85
|
+
Demo
|
|
86
|
+
----
|
|
87
|
+
AIM-CU can also be run through the demo available at https://huggingface.co/spaces/didsr/AIM-CU. If Space is paused, click on Restart button. Note: this Space uses a custom Docker container; build may break due to latest package updates pulled by HuggingFace.
|
|
88
|
+
|
|
89
|
+
Usability
|
|
90
|
+
---------
|
|
91
|
+
* Example AI output CSV file is available as `config/spec-60-60.csv <config/spec-60-60.csv>`_ to be uploaded in monitoring phase.
|
|
92
|
+
|
|
93
|
+
* Workflow instruction to run the tool is available at bottom-left of UI.
|
|
94
|
+
|
|
95
|
+
* Sample UI output is available at `assets/ui.png <assets/ui.png>`_.
|
|
96
|
+
|
|
97
|
+
* Setting ``control:save_figure`` to ``true`` from `config.toml <config/config.toml>`_ will save tables and plots in `figure/ <figure/>`_.
|
|
98
|
+
|
|
99
|
+
* Running AIM-CU usually only takes a few seconds, and it does not require a GPU to run.
|
|
100
|
+
|
|
101
|
+
Related References
|
|
102
|
+
------------------
|
|
103
|
+
* Smriti Prathapan, Ravi K. Samala, Nathan Hadjiyski, Pierre‑François D’Haese, Nicholas Petrick, Jana Delfino, Fabien Maldonado, Brandon Nelson, Ghada Zamzmi, Phuong Nguyen, Yelena Yesha, and Berkman Sahiner, "Detecting performance drift in AI models for medical image analysis using CUSUM chart" (Journal in press 2026)
|
|
104
|
+
|
|
105
|
+
* Prathapan, S., Sahiner, B., Kadia, D., and Samala, R.K. 2025, "AIM-CU: A statistical tool for AI Monitoring." (Journal in-press 2026)
|
|
106
|
+
|
|
107
|
+
* Prathapan, S., Samala, R.K., Hadjiyski, N., D’Haese, P.F., Maldonado, F., Nguyen, P., Yesha, Y. and Sahiner, B., 2024, April. Quantifying input data drift in medical machine learning models by detecting change-points in time-series data. In Medical Imaging 2024: Computer-Aided Diagnosis (Vol. 12927, pp. 67-76). SPIE. https://doi.org/10.1117/12.3008771
|
|
108
|
+
|
|
109
|
+
* Smriti Prathapan, Ravi K. Samala, Nathan Hadjiyski, Pierre‑François D’Haese, Nicholas Petrick, Jana Delfino, Fabien Maldonado, Brandon Nelson, Ghada Zamzmi, Phuong Nguyen, Yelena Yesha, and Berkman Sahiner, "Post-market Monitoring of AI-enabled Medical Devices for Radiology and Healthcare Applications" (FDA-UMiami Collaboration Poster, September 2023)
|
|
110
|
+
|
|
111
|
+
Disclaimer
|
|
112
|
+
---------------------
|
|
113
|
+
|
|
114
|
+
**About the Catalog of Regulatory Science Tools**
|
|
115
|
+
|
|
116
|
+
The enclosed tool is *in preparation* for the `Catalog of Regulatory Science Tools <https://cdrh-rst.fda.gov>`_. Note that this software is not (yet) a standalone RST but rather is to be included as a **reference tool** to support other Regulatory Science Tools (such as synthetic datasets) for reproducibility.
|
|
117
|
+
|
|
118
|
+
This catalog collates a variety of regulatory science tools that the FDA's Center for Devices and Radiological Health's (CDRH) Office of Science and Engineering Labs (OSEL) developed. These tools use the most innovative science to support medical device development and patient access to safe and effective medical devices. If you are considering using a tool from this catalog in your marketing submissions, note that these tools have not been qualified as `Medical Device Development Tools <https://www.fda.gov/medical-devices/medical-device-development-tools-mddt>`_ and the FDA has not evaluated the suitability of these tools within any specific context of use. You may `request feedback or meetings for medical device submissions <https://www.fda.gov/regulatory-information/search-fda-guidance-documents/requests-feedback-and-meetings-medical-device-submissions-q-submission-program>`_ as part of the Q-Submission Program.
|
|
119
|
+
|
|
120
|
+
For more information about the Catalog of Regulatory Science Tools, email RST_CDRH@fda.hhs.gov.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
AIM-CU: A CUSUM-based tool for AI Monitoring
|
|
2
|
+
============================================
|
|
3
|
+
Monitoring a clinically deployed AI device to detect performance drift is an essential step to ensure the safety and effectiveness of AI.
|
|
4
|
+
|
|
5
|
+
AIM-CU is a statistical tool for AI monitoring based on a cumulative sum (AIM-CU) approach.
|
|
6
|
+
|
|
7
|
+
AIM-CU computes:
|
|
8
|
+
|
|
9
|
+
* The parameter choices for change-point detection based on an acceptable false alarm rate
|
|
10
|
+
* Detection delay estimates for a given displacement of the performance metric from the target for those parameter choices.
|
|
11
|
+
|
|
12
|
+
System setup
|
|
13
|
+
------------
|
|
14
|
+
Make sure R is installed in the system. There is no specific version that this relies on. Here we have used the version 4.1.2 (2021-11-01).
|
|
15
|
+
Instructions for Linux (the below setup is only performed in Linux):
|
|
16
|
+
|
|
17
|
+
.. code-block:: shell
|
|
18
|
+
|
|
19
|
+
wget -qO- https://cloud.r-project.org/bin/linux/ubuntu/marutter_pubkey.asc | tee -a /etc/apt/trusted.gpg.d/cran_ubuntu_key.asc
|
|
20
|
+
add-apt-repository "deb https://cloud.r-project.org/bin/linux/ubuntu $(lsb_release -cs)-cran40/"
|
|
21
|
+
apt-get install -y --no-install-recommends r-base r-base-dev
|
|
22
|
+
|
|
23
|
+
# setup R configs
|
|
24
|
+
echo "r <- getOption('repos'); r['CRAN'] <- 'http://cran.us.r-project.org'; options(repos = r);" > ~/.Rprofile
|
|
25
|
+
Rscript -e "install.packages('ggplot2')"
|
|
26
|
+
Rscript -e "install.packages('hexbin')"
|
|
27
|
+
Rscript -e "install.packages('lazyeval')"
|
|
28
|
+
Rscript -e "install.packages('cusumcharter')"
|
|
29
|
+
Rscript -e "install.packages('RcppCNPy')"
|
|
30
|
+
Rscript -e "install.packages('spc')"
|
|
31
|
+
|
|
32
|
+
Code execution
|
|
33
|
+
--------------
|
|
34
|
+
Clone AIM-CU repository.
|
|
35
|
+
|
|
36
|
+
.. code-block:: shell
|
|
37
|
+
|
|
38
|
+
git clone https://github.com/DIDSR/AIM-CU.git
|
|
39
|
+
|
|
40
|
+
Run the following commands to install required dependencies (Python = 3.10 is used).
|
|
41
|
+
|
|
42
|
+
.. code-block:: shell
|
|
43
|
+
|
|
44
|
+
apt-get -y install python3
|
|
45
|
+
apt-get -y install pip
|
|
46
|
+
cd AIM-CU
|
|
47
|
+
pip install -r requirements.txt
|
|
48
|
+
|
|
49
|
+
Run AIM-CU.
|
|
50
|
+
|
|
51
|
+
.. code-block:: shell
|
|
52
|
+
|
|
53
|
+
cd src/package
|
|
54
|
+
python3 app.py
|
|
55
|
+
|
|
56
|
+
Open the URL http://0.0.0.0:7860 that is running the AIM-CU locally.
|
|
57
|
+
|
|
58
|
+
Example code execution
|
|
59
|
+
----------------------
|
|
60
|
+
Example code can be run through Jupyter Notebook. Do this by entering the ``jupyter notebook`` command from the ``/src/package/`` directory. The tool is designed to be used through a GUI, not from the console.
|
|
61
|
+
|
|
62
|
+
Demo
|
|
63
|
+
----
|
|
64
|
+
AIM-CU can also be run through the demo available at https://huggingface.co/spaces/didsr/AIM-CU. If Space is paused, click on Restart button. Note: this Space uses a custom Docker container; build may break due to latest package updates pulled by HuggingFace.
|
|
65
|
+
|
|
66
|
+
Usability
|
|
67
|
+
---------
|
|
68
|
+
* Example AI output CSV file is available as `config/spec-60-60.csv <config/spec-60-60.csv>`_ to be uploaded in monitoring phase.
|
|
69
|
+
|
|
70
|
+
* Workflow instruction to run the tool is available at bottom-left of UI.
|
|
71
|
+
|
|
72
|
+
* Sample UI output is available at `assets/ui.png <assets/ui.png>`_.
|
|
73
|
+
|
|
74
|
+
* Setting ``control:save_figure`` to ``true`` from `config.toml <config/config.toml>`_ will save tables and plots in `figure/ <figure/>`_.
|
|
75
|
+
|
|
76
|
+
* Running AIM-CU usually only takes a few seconds, and it does not require a GPU to run.
|
|
77
|
+
|
|
78
|
+
Related References
|
|
79
|
+
------------------
|
|
80
|
+
* Smriti Prathapan, Ravi K. Samala, Nathan Hadjiyski, Pierre‑François D’Haese, Nicholas Petrick, Jana Delfino, Fabien Maldonado, Brandon Nelson, Ghada Zamzmi, Phuong Nguyen, Yelena Yesha, and Berkman Sahiner, "Detecting performance drift in AI models for medical image analysis using CUSUM chart" (Journal in press 2026)
|
|
81
|
+
|
|
82
|
+
* Prathapan, S., Sahiner, B., Kadia, D., and Samala, R.K. 2025, "AIM-CU: A statistical tool for AI Monitoring." (Journal in-press 2026)
|
|
83
|
+
|
|
84
|
+
* Prathapan, S., Samala, R.K., Hadjiyski, N., D’Haese, P.F., Maldonado, F., Nguyen, P., Yesha, Y. and Sahiner, B., 2024, April. Quantifying input data drift in medical machine learning models by detecting change-points in time-series data. In Medical Imaging 2024: Computer-Aided Diagnosis (Vol. 12927, pp. 67-76). SPIE. https://doi.org/10.1117/12.3008771
|
|
85
|
+
|
|
86
|
+
* Smriti Prathapan, Ravi K. Samala, Nathan Hadjiyski, Pierre‑François D’Haese, Nicholas Petrick, Jana Delfino, Fabien Maldonado, Brandon Nelson, Ghada Zamzmi, Phuong Nguyen, Yelena Yesha, and Berkman Sahiner, "Post-market Monitoring of AI-enabled Medical Devices for Radiology and Healthcare Applications" (FDA-UMiami Collaboration Poster, September 2023)
|
|
87
|
+
|
|
88
|
+
Disclaimer
|
|
89
|
+
---------------------
|
|
90
|
+
|
|
91
|
+
**About the Catalog of Regulatory Science Tools**
|
|
92
|
+
|
|
93
|
+
The enclosed tool is *in preparation* for the `Catalog of Regulatory Science Tools <https://cdrh-rst.fda.gov>`_. Note that this software is not (yet) a standalone RST but rather is to be included as a **reference tool** to support other Regulatory Science Tools (such as synthetic datasets) for reproducibility.
|
|
94
|
+
|
|
95
|
+
This catalog collates a variety of regulatory science tools that the FDA's Center for Devices and Radiological Health's (CDRH) Office of Science and Engineering Labs (OSEL) developed. These tools use the most innovative science to support medical device development and patient access to safe and effective medical devices. If you are considering using a tool from this catalog in your marketing submissions, note that these tools have not been qualified as `Medical Device Development Tools <https://www.fda.gov/medical-devices/medical-device-development-tools-mddt>`_ and the FDA has not evaluated the suitability of these tools within any specific context of use. You may `request feedback or meetings for medical device submissions <https://www.fda.gov/regulatory-information/search-fda-guidance-documents/requests-feedback-and-meetings-medical-device-submissions-q-submission-program>`_ as part of the Q-Submission Program.
|
|
96
|
+
|
|
97
|
+
For more information about the Catalog of Regulatory Science Tools, email RST_CDRH@fda.hhs.gov.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting Security Vulnerabilities
|
|
4
|
+
|
|
5
|
+
The U.S. Food and Drug Administration (FDA) takes security vulnerabilities seriously. If you believe you have found a security vulnerability in this repository, please report it to us through coordinated disclosure.
|
|
6
|
+
|
|
7
|
+
**Please do NOT report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## How to Report
|
|
12
|
+
|
|
13
|
+
FDA follows the HHS Vulnerability Disclosure Policy. Submit your report through the official HHS reporting portal:
|
|
14
|
+
|
|
15
|
+
- **HHS Vulnerability Disclosure Policy:** [https://www.hhs.gov/vulnerability-disclosure-policy/index.html]
|
|
16
|
+
- **Submit a Report:** [https://hhs.responsibledisclosure.com]
|
|
17
|
+
|
|
18
|
+
### Contact
|
|
19
|
+
|
|
20
|
+
For general security questions or concerns about FDA systems, contact:
|
|
21
|
+
- **Repository Maintainer:** [RST_CDRH@fda.hhs.gov]
|
|
22
|
+
- **Security Maintainer:** [ciocc@fda.hhs.gov]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
## Security Best Practices for Contributors
|
|
27
|
+
|
|
28
|
+
When contributing to this repository:
|
|
29
|
+
|
|
30
|
+
- Follow the principle of least privilege
|
|
31
|
+
- Validate and sanitize all inputs
|
|
32
|
+
- Use parameterized queries for database access
|
|
33
|
+
- Keep dependencies up to date
|
|
34
|
+
- Never commit secrets, API keys, or credentials
|
|
35
|
+
- Review security advisories for dependencies
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# aim_cu/__init__.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
AIM-CU: CUSUM-based AI performance monitoring.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .aim_cu import (
|
|
8
|
+
CUSUM,
|
|
9
|
+
compute_ARL1,
|
|
10
|
+
compute_ARL1_table,
|
|
11
|
+
get_ref_value,
|
|
12
|
+
get_ref_values,
|
|
13
|
+
get_threshold,
|
|
14
|
+
load_package_config,
|
|
15
|
+
shift_in_mean,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CUSUM",
|
|
20
|
+
"compute_ARL1",
|
|
21
|
+
"compute_ARL1_table",
|
|
22
|
+
"get_ref_value",
|
|
23
|
+
"get_ref_values",
|
|
24
|
+
"get_threshold",
|
|
25
|
+
"load_package_config",
|
|
26
|
+
"shift_in_mean",
|
|
27
|
+
]
|
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIM-CU is a statistical tool for AI monitoring based on a cumulative sum (AIM-CU) approach.
|
|
3
|
+
|
|
4
|
+
AIM-CU computes:
|
|
5
|
+
- The parameter choices for change-point detection based on an acceptable false alarm rate
|
|
6
|
+
- Detection delay estimates for a given displacement of the performance metric from the target for those parameter choices.
|
|
7
|
+
|
|
8
|
+
Notes:
|
|
9
|
+
- Requires: rpy2, an R installation, and R package `spc` (plus optional ggplot2/hexbin/lazyeval/cusumcharter/RcppCNPy).
|
|
10
|
+
- If R packages are missing, this code will attempt to install them (CRAN mirror must be reachable).
|
|
11
|
+
"""
|
|
12
|
+
#Packages
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import random
|
|
16
|
+
import warnings
|
|
17
|
+
from collections import OrderedDict
|
|
18
|
+
from typing import Tuple, List
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import pandas as pd
|
|
22
|
+
import tomli
|
|
23
|
+
import matplotlib
|
|
24
|
+
matplotlib.use("Agg")
|
|
25
|
+
import matplotlib.pyplot as plt
|
|
26
|
+
from importlib.resources import files
|
|
27
|
+
|
|
28
|
+
#Read config.toml
|
|
29
|
+
def load_package_config():
|
|
30
|
+
config_path = files("aim_cu").joinpath("config.toml")
|
|
31
|
+
with config_path.open("rb") as f:
|
|
32
|
+
return tomli.load(f)
|
|
33
|
+
|
|
34
|
+
_CONFIG = load_package_config()
|
|
35
|
+
#print("_CONFIG =", _CONFIG)
|
|
36
|
+
shift_in_mean = _CONFIG["CUSUM_params"]["shift_in_mean"]
|
|
37
|
+
# ---------------------------
|
|
38
|
+
# R / rpy2 setup (spc package)
|
|
39
|
+
# ---------------------------
|
|
40
|
+
import rpy2.robjects as ro
|
|
41
|
+
import rpy2.robjects.packages as rpackages
|
|
42
|
+
|
|
43
|
+
#---new imports
|
|
44
|
+
# import R's utility package
|
|
45
|
+
utils = rpackages.importr('utils')
|
|
46
|
+
spc = rpackages.importr('spc')
|
|
47
|
+
|
|
48
|
+
# Suppress all R warnings globally
|
|
49
|
+
ro.r["options"](warn=-1)
|
|
50
|
+
|
|
51
|
+
#warnings.filterwarnings("ignore")
|
|
52
|
+
warnings.filterwarnings("ignore", module=r"rpy2\..*")
|
|
53
|
+
random.seed(58)
|
|
54
|
+
|
|
55
|
+
# --------------------------------------------------------
|
|
56
|
+
# I Compute CUSUM parameters: h,k and average run length
|
|
57
|
+
# --------------------------------------------------------
|
|
58
|
+
def get_ref_value(h: float, ARL_0: float) -> float:
|
|
59
|
+
"""
|
|
60
|
+
Compute the normalized reference value (k) for a given threshold (h) and in-control Average Run Length (ARL₀).
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
h : float
|
|
65
|
+
Normalized decision threshold of the CUSUM chart.
|
|
66
|
+
ARL_0 : float
|
|
67
|
+
Target in-control Average Run Length (ARL₀).
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
float
|
|
72
|
+
Normalized reference value (k) corresponding to the given h and ARL₀.
|
|
73
|
+
"""
|
|
74
|
+
k = np.round(spc.xcusum_crit_L0h(ARL_0, h), decimals=4).tolist()[0]
|
|
75
|
+
return k
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_threshold(k: float, ARL_0: float) -> float:
|
|
79
|
+
"""
|
|
80
|
+
Compute the normalized threshold (h) for a given reference value (k)
|
|
81
|
+
and target in-control Average Run Length (ARL₀).
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
k : float
|
|
86
|
+
Normalized reference value used in the CUSUM chart.
|
|
87
|
+
ARL_0 : float
|
|
88
|
+
Target in-control Average Run Length (ARL₀).
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
float
|
|
93
|
+
Normalized threshold (h) corresponding to the given k and ARL₀.
|
|
94
|
+
"""
|
|
95
|
+
h = np.round(
|
|
96
|
+
spc.xcusum_crit_(k, ARL_0, mu0=0, hs=0, sided="one", r=30),
|
|
97
|
+
decimals=4,
|
|
98
|
+
).tolist()[0]
|
|
99
|
+
return h
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_ref_values(h: float, list_ARL_0: List[float]) -> Tuple[pd.DataFrame, OrderedDict]:
|
|
103
|
+
"""
|
|
104
|
+
Compute normalized reference values (k) for a list of target ARL₀ values
|
|
105
|
+
given a fixed threshold (h).
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
h : float
|
|
110
|
+
Normalized decision threshold of the CUSUM chart.
|
|
111
|
+
list_ARL_0 : List[float]
|
|
112
|
+
List of target in-control Average Run Length (ARL₀) values.
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
Tuple[pandas.DataFrame, collections.OrderedDict]
|
|
117
|
+
A tuple containing:
|
|
118
|
+
- DataFrame: Table with columns ['ARL_0', 'k'] listing each ARL₀ and its corresponding k.
|
|
119
|
+
- OrderedDict: Mapping of ARL₀ to k
|
|
120
|
+
"""
|
|
121
|
+
dict_ARL0_k: OrderedDict = OrderedDict()
|
|
122
|
+
summary_table_df_ARL0_k = pd.DataFrame(columns=["ARL_0", "k"])
|
|
123
|
+
|
|
124
|
+
for n, ARL_0 in enumerate(list_ARL_0):
|
|
125
|
+
k = np.round(spc.xcusum_crit_L0h(ARL_0, h), decimals=4).tolist()[0]
|
|
126
|
+
summary_table_df_ARL0_k.loc[n] = [ARL_0, k]
|
|
127
|
+
dict_ARL0_k[ARL_0] = k
|
|
128
|
+
|
|
129
|
+
return summary_table_df_ARL0_k, dict_ARL0_k
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def compute_ARL1(h: float, k: float, mu1: float) -> float:
|
|
133
|
+
"""
|
|
134
|
+
Compute the out-of-control Average Run Length (ARL₁) for a given
|
|
135
|
+
threshold (h), reference value (k), and mean shift (μ₁).
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
h : float
|
|
140
|
+
Normalized decision threshold.
|
|
141
|
+
k : float
|
|
142
|
+
Normalized reference value.
|
|
143
|
+
mu1 : float
|
|
144
|
+
Change in the process mean (standard deviations from the target value).
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
float
|
|
149
|
+
Average Run Length (ARL₁), is the estimate of steady state ARL (expected detection delay) to detect the change in mean.
|
|
150
|
+
"""
|
|
151
|
+
ARL_1 = np.round(
|
|
152
|
+
spc.xcusum_ad_(k=k, h=h, mu1=mu1, mu0=0, sided="two", r=20),
|
|
153
|
+
decimals=2,
|
|
154
|
+
).tolist()[0]
|
|
155
|
+
return ARL_1
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def compute_ARL1_table(h: float, shift_in_mean: List[float], dict_ARL0_k: OrderedDict) -> pd.DataFrame:
|
|
159
|
+
"""
|
|
160
|
+
Compute a table of Average Run Length (ARL₁) values across multiple change in mean and ARL₀-reference value (k) combinations.
|
|
161
|
+
|
|
162
|
+
Parameters
|
|
163
|
+
----------
|
|
164
|
+
h : float
|
|
165
|
+
Normalized decision threshold.
|
|
166
|
+
shift_in_mean : List[float]
|
|
167
|
+
List of mean shifts (μ₁), expressed in standardized units.
|
|
168
|
+
dict_ARL0_k : OrderedDict
|
|
169
|
+
Mapping of target in-control ARL₀ values to their corresponding
|
|
170
|
+
normalized reference values (k).
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
pandas.DataFrame
|
|
175
|
+
Table of ARL₁ values for mean shifts and corresponding ARL₀-k combinations.
|
|
176
|
+
"""
|
|
177
|
+
list_ARL_0 = list(dict_ARL0_k.keys())
|
|
178
|
+
|
|
179
|
+
dict_data_ARL1_k: OrderedDict = OrderedDict()
|
|
180
|
+
dict_data_ARL1_k["Shift in mean"] = shift_in_mean
|
|
181
|
+
|
|
182
|
+
for ARL_0 in list_ARL_0:
|
|
183
|
+
k = dict_ARL0_k[ARL_0]
|
|
184
|
+
list_ARL_1 = []
|
|
185
|
+
for mu1 in shift_in_mean:
|
|
186
|
+
ARL_1 = np.round(
|
|
187
|
+
spc.xcusum_ad_(k=k, h=h, mu1=mu1, mu0=0, sided="two", r=20),
|
|
188
|
+
decimals=2,
|
|
189
|
+
).tolist()[0]
|
|
190
|
+
list_ARL_1.append(ARL_1)
|
|
191
|
+
|
|
192
|
+
#Format column name: "50 (0.16)"
|
|
193
|
+
col_name = f"{int(ARL_0)} ({k:.2f})"
|
|
194
|
+
dict_data_ARL1_k[col_name] = list_ARL_1
|
|
195
|
+
|
|
196
|
+
#dict_data_ARL1_k[ARL_0] = list_ARL_1
|
|
197
|
+
|
|
198
|
+
return pd.DataFrame(dict_data_ARL1_k)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# -------------------------------------------
|
|
202
|
+
# II Performance drift detection using CUSUM
|
|
203
|
+
# -------------------------------------------
|
|
204
|
+
class CUSUM:
|
|
205
|
+
"""
|
|
206
|
+
CUSUM-based performance drift detection.
|
|
207
|
+
|
|
208
|
+
This class provides methods to initialize baseline parameters, compute
|
|
209
|
+
positive and negative CUSUM statistics, detect performance drifts, and
|
|
210
|
+
generate CUSUM charts.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
def __init__(self):
|
|
214
|
+
self.df_metric = None
|
|
215
|
+
self.metric_type = None
|
|
216
|
+
|
|
217
|
+
self.AvgDD = None
|
|
218
|
+
self.data = None
|
|
219
|
+
|
|
220
|
+
self.H = None
|
|
221
|
+
self.in_std = None
|
|
222
|
+
self.in_mu = None
|
|
223
|
+
self.S_hi = None
|
|
224
|
+
self.S_lo = None
|
|
225
|
+
|
|
226
|
+
self.config = None
|
|
227
|
+
|
|
228
|
+
self.total_days = None
|
|
229
|
+
self.pre_change_days = None
|
|
230
|
+
self.post_change_days = None
|
|
231
|
+
|
|
232
|
+
self.init_days = None
|
|
233
|
+
|
|
234
|
+
def initialize(self) -> None:
|
|
235
|
+
"""
|
|
236
|
+
Load and initialize configuration settings from the configuration file (`config.toml`).
|
|
237
|
+
|
|
238
|
+
This method populates the 'config' attribute with parameters such as plotting options, and default values.
|
|
239
|
+
|
|
240
|
+
Returns
|
|
241
|
+
-------
|
|
242
|
+
None
|
|
243
|
+
"""
|
|
244
|
+
try:
|
|
245
|
+
path_file_config = os.path.abspath("config.toml")
|
|
246
|
+
with open(path_file_config, "rb") as file_config:
|
|
247
|
+
self.config = tomli.load(file_config)
|
|
248
|
+
except FileNotFoundError:
|
|
249
|
+
print("Error: config.toml not found at", path_file_config)
|
|
250
|
+
sys.exit(1)
|
|
251
|
+
|
|
252
|
+
def set_init_stats(self, init_days: int) -> None:
|
|
253
|
+
"""
|
|
254
|
+
Compute in-control parameters from baseline observations.
|
|
255
|
+
|
|
256
|
+
Uses the first 'init_days' observations to estimate the in-control mean and standard deviation.
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
----------
|
|
260
|
+
init_days : int
|
|
261
|
+
Number of observations assumed to be in-control.
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
None
|
|
266
|
+
"""
|
|
267
|
+
self.init_days = init_days
|
|
268
|
+
in_control_data = self.data[: self.init_days]
|
|
269
|
+
self.in_std = float(np.std(in_control_data))
|
|
270
|
+
self.in_mu = float(np.mean(in_control_data))
|
|
271
|
+
|
|
272
|
+
def set_timeline(self, data: np.ndarray) -> None:
|
|
273
|
+
"""
|
|
274
|
+
Set the timeline of the performance metric.
|
|
275
|
+
|
|
276
|
+
Determines the total number of observations in the input data.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
data : np.ndarray
|
|
281
|
+
1D Array of the AI performance metric.
|
|
282
|
+
|
|
283
|
+
Returns
|
|
284
|
+
-------
|
|
285
|
+
None
|
|
286
|
+
"""
|
|
287
|
+
self.total_days = int(np.shape(data)[0])
|
|
288
|
+
|
|
289
|
+
def set_df_metric_default(self) -> None:
|
|
290
|
+
"""
|
|
291
|
+
Read the AI performance metric from the default CSV file and initializes the internal
|
|
292
|
+
data and set the timeline.
|
|
293
|
+
|
|
294
|
+
Raises
|
|
295
|
+
------
|
|
296
|
+
SystemExit
|
|
297
|
+
If the CSV file is not found at the specified path.
|
|
298
|
+
|
|
299
|
+
Returns
|
|
300
|
+
-------
|
|
301
|
+
None
|
|
302
|
+
"""
|
|
303
|
+
try:
|
|
304
|
+
path_csv = os.path.abspath(os.path.join("../../", self.config["path_input"]["path_df_metric"]))
|
|
305
|
+
self.df_metric = pd.read_csv(path_csv)
|
|
306
|
+
except FileNotFoundError:
|
|
307
|
+
print("Error: CSV file not found at", path_csv)
|
|
308
|
+
sys.exit(1)
|
|
309
|
+
|
|
310
|
+
self.data = self.df_metric[self.df_metric.columns[1]].to_numpy()
|
|
311
|
+
self.set_timeline(self.data)
|
|
312
|
+
|
|
313
|
+
def set_df_metric_csv(self, data_csv: pd.DataFrame) -> None:
|
|
314
|
+
"""
|
|
315
|
+
Assign the performance metric data read fom the input csv file to a datafram .
|
|
316
|
+
Parameters
|
|
317
|
+
----------
|
|
318
|
+
data_csv : pandas.DataFrame
|
|
319
|
+
DataFrame containing the performance metric data. The values are expected in the second column (index 1).
|
|
320
|
+
|
|
321
|
+
Returns
|
|
322
|
+
-------
|
|
323
|
+
None
|
|
324
|
+
"""
|
|
325
|
+
self.df_metric = data_csv
|
|
326
|
+
self.data = self.df_metric[self.df_metric.columns[1]].to_numpy()
|
|
327
|
+
self.set_timeline(self.data)
|
|
328
|
+
|
|
329
|
+
def compute_cusum(
|
|
330
|
+
self, x: List[float], mu_0: float, ref_val: float
|
|
331
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
332
|
+
"""
|
|
333
|
+
Compute CUSUM statistics for the performance metric.
|
|
334
|
+
|
|
335
|
+
Calculates the positive (S_hi) and negative (S_lo) cumulative sums,
|
|
336
|
+
along with the cumulative deviation from the in-control mean, to
|
|
337
|
+
monitor for shifts in the process.
|
|
338
|
+
|
|
339
|
+
Parameters
|
|
340
|
+
----------
|
|
341
|
+
x : List[float]
|
|
342
|
+
Sequence of observed performance metric values.
|
|
343
|
+
mu_0 : float
|
|
344
|
+
In-control mean (baseline) of the performance metric.
|
|
345
|
+
ref_val : float
|
|
346
|
+
Reference value (k) that determines sensitivity to shifts.
|
|
347
|
+
|
|
348
|
+
Returns
|
|
349
|
+
-------
|
|
350
|
+
Tuple[np.ndarray, np.ndarray, np.ndarray]
|
|
351
|
+
A tuple containing:
|
|
352
|
+
- S_hi : Positive CUSUM values (detects upward shifts)
|
|
353
|
+
- S_lo : Negative CUSUM values (detects downward shifts)
|
|
354
|
+
- cusum : Cumulative sum of deviations from the mean
|
|
355
|
+
"""
|
|
356
|
+
x = np.asarray(x, dtype=float)
|
|
357
|
+
num_rows = x.shape[0]
|
|
358
|
+
|
|
359
|
+
x_mean = np.zeros(num_rows, dtype=float)
|
|
360
|
+
self.S_hi = np.zeros(num_rows, dtype=float)
|
|
361
|
+
self.S_lo = np.zeros(num_rows, dtype=float)
|
|
362
|
+
cusum = np.zeros(num_rows, dtype=float)
|
|
363
|
+
|
|
364
|
+
# Starts with 0
|
|
365
|
+
self.S_hi[0] = 0.0
|
|
366
|
+
self.S_lo[0] = 0.0
|
|
367
|
+
cusum[0] = 0.0
|
|
368
|
+
|
|
369
|
+
mean_hi = np.zeros(num_rows, dtype=float)
|
|
370
|
+
mean_lo = np.zeros(num_rows, dtype=float)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
x_mean[0] = x[0] - mu_0
|
|
374
|
+
mean_hi[0] = x[0] - mu_0 - ref_val
|
|
375
|
+
mean_lo[0] = mu_0 - ref_val - x[0]
|
|
376
|
+
|
|
377
|
+
for i in range(1, num_rows):
|
|
378
|
+
x_mean[i] = x[i] - mu_0
|
|
379
|
+
mean_hi[i] = x[i] - mu_0 - ref_val
|
|
380
|
+
self.S_hi[i] = max(0.0, self.S_hi[i - 1] + mean_hi[i])
|
|
381
|
+
mean_lo[i] = mu_0 - ref_val - x[i]
|
|
382
|
+
self.S_lo[i] = max(0.0, self.S_lo[i - 1] + mean_lo[i])
|
|
383
|
+
cusum[i] = cusum[i - 1] + x_mean[i]
|
|
384
|
+
|
|
385
|
+
return (
|
|
386
|
+
np.round(self.S_hi, 2),
|
|
387
|
+
np.round(self.S_lo, 2),
|
|
388
|
+
np.round(cusum, 2),
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def change_detection(
|
|
392
|
+
self,
|
|
393
|
+
normalized_ref_value: float = 0.5,
|
|
394
|
+
normalized_threshold: float = 4,
|
|
395
|
+
) -> None:
|
|
396
|
+
"""
|
|
397
|
+
Detect changes in the process using CUSUM statistics.
|
|
398
|
+
|
|
399
|
+
Computes the CUSUM values and identifies the first point at which
|
|
400
|
+
the process deviates significantly from the in-control state based
|
|
401
|
+
on the specified reference value and threshold.
|
|
402
|
+
|
|
403
|
+
Parameters
|
|
404
|
+
----------
|
|
405
|
+
normalized_ref_value : float, optional
|
|
406
|
+
Normalized reference value (k) that controls sensitivity to
|
|
407
|
+
shifts in the process mean. Default is 0.5.
|
|
408
|
+
normalized_threshold : float, optional
|
|
409
|
+
Normalized decision threshold (h) used to signal a change.
|
|
410
|
+
Default is 4.
|
|
411
|
+
Returns
|
|
412
|
+
-------
|
|
413
|
+
None
|
|
414
|
+
"""
|
|
415
|
+
self.pre_change_days = None
|
|
416
|
+
|
|
417
|
+
control_limit = normalized_threshold
|
|
418
|
+
self.H = control_limit * self.in_std
|
|
419
|
+
ref_val = normalized_ref_value * self.in_std
|
|
420
|
+
|
|
421
|
+
x = np.array(self.data, dtype=float)
|
|
422
|
+
self.S_hi, self.S_lo, _ = self.compute_cusum(x, self.in_mu, ref_val)
|
|
423
|
+
|
|
424
|
+
# Find first occurrence where the threshold is exceeded
|
|
425
|
+
S_hi_exceeds = np.where(self.S_hi > self.H)[0]
|
|
426
|
+
S_lo_exceeds = np.where(self.S_lo > self.H)[0]
|
|
427
|
+
|
|
428
|
+
# Detect whether S_hi or S_lo exceeds threshold
|
|
429
|
+
if len(S_hi_exceeds) > 0 and len(S_lo_exceeds) > 0:
|
|
430
|
+
if S_hi_exceeds[0] < S_lo_exceeds[0]:
|
|
431
|
+
self.pre_change_days = int(S_hi_exceeds[0])
|
|
432
|
+
print(f"Detected upward drift at: {self.pre_change_days}")
|
|
433
|
+
else:
|
|
434
|
+
self.pre_change_days = int(S_lo_exceeds[0])
|
|
435
|
+
print(f"Detected downward drift at: {self.pre_change_days}")
|
|
436
|
+
elif len(S_hi_exceeds) > 0:
|
|
437
|
+
self.pre_change_days = int(S_hi_exceeds[0])
|
|
438
|
+
print(f"Detected upward drift at: {self.pre_change_days}")
|
|
439
|
+
elif len(S_lo_exceeds) > 0:
|
|
440
|
+
self.pre_change_days = int(S_lo_exceeds[0])
|
|
441
|
+
print(f"Detected downward drift at: {self.pre_change_days}")
|
|
442
|
+
else:
|
|
443
|
+
print("No performance drift detected")
|
|
444
|
+
|
|
445
|
+
def plot_input_data(self):
|
|
446
|
+
"""
|
|
447
|
+
Plot the AI performance metric with baseline region highlighted.
|
|
448
|
+
|
|
449
|
+
Generates a scatter plot of the performance metric over time
|
|
450
|
+
|
|
451
|
+
Returns
|
|
452
|
+
-------
|
|
453
|
+
matplotlib.figure.Figure
|
|
454
|
+
Matplotlib figure containing the scatter plot of the data.
|
|
455
|
+
"""
|
|
456
|
+
x1 = np.arange(self.init_days)
|
|
457
|
+
y1 = self.data[: self.init_days]
|
|
458
|
+
|
|
459
|
+
x2 = np.arange(self.init_days, self.total_days, 1)
|
|
460
|
+
y2 = self.data[self.init_days : self.total_days]
|
|
461
|
+
|
|
462
|
+
fig, ax = plt.subplots(figsize=(10, 5))
|
|
463
|
+
|
|
464
|
+
# Scatter plots
|
|
465
|
+
ax.scatter(x1, y1, color="lime", s=20, alpha=0.4)
|
|
466
|
+
ax.scatter(x2, y2, color="lime", s=20, alpha=0.2)
|
|
467
|
+
|
|
468
|
+
# Highlight baseline
|
|
469
|
+
ax.axvspan(0, self.init_days, color="palegreen", alpha=0.25)
|
|
470
|
+
|
|
471
|
+
# Labels and title
|
|
472
|
+
ax.set_title("AI output", fontsize=16, fontweight="bold")
|
|
473
|
+
ax.set_xlabel("Time", fontsize=14, fontweight="bold")
|
|
474
|
+
ax.set_ylabel("AI model metric", fontsize=14, fontweight="bold")
|
|
475
|
+
|
|
476
|
+
# Tick spacing (similar to dtick=20)
|
|
477
|
+
ax.set_xticks(np.arange(0, self.total_days, 20))
|
|
478
|
+
|
|
479
|
+
# Background color (optional)
|
|
480
|
+
try:
|
|
481
|
+
ax.set_facecolor(self.config["color"]["blue_005"])
|
|
482
|
+
except Exception:
|
|
483
|
+
pass # fallback if config not set
|
|
484
|
+
|
|
485
|
+
# Remove legend
|
|
486
|
+
ax.legend().set_visible(False) if ax.get_legend() else None
|
|
487
|
+
|
|
488
|
+
plt.tight_layout()
|
|
489
|
+
|
|
490
|
+
return fig
|
|
491
|
+
|
|
492
|
+
def plot_changepoint(self):
|
|
493
|
+
"""
|
|
494
|
+
Plot the AI performance metric with the detected change pointhighlighting in-control and out-of-control regions
|
|
495
|
+
and marking the detected change point.
|
|
496
|
+
|
|
497
|
+
Returns
|
|
498
|
+
-------
|
|
499
|
+
matplotlib.figure.Figure
|
|
500
|
+
Matplotlib figure containing the change-point plot.
|
|
501
|
+
"""
|
|
502
|
+
|
|
503
|
+
# Determine change-point
|
|
504
|
+
if self.pre_change_days is None:
|
|
505
|
+
split = self.init_days
|
|
506
|
+
else:
|
|
507
|
+
split = self.pre_change_days
|
|
508
|
+
|
|
509
|
+
# Split data
|
|
510
|
+
x1 = np.arange(split)
|
|
511
|
+
y1 = self.data[:split]
|
|
512
|
+
mean_y1 = np.mean(y1) if len(y1) else 0.0
|
|
513
|
+
|
|
514
|
+
x2 = np.arange(split, self.total_days)
|
|
515
|
+
y2 = self.data[split:self.total_days]
|
|
516
|
+
mean_y2 = np.mean(y2) if len(y2) else 0.0
|
|
517
|
+
|
|
518
|
+
fig, ax = plt.subplots(figsize=(10, 5))
|
|
519
|
+
|
|
520
|
+
# Scatter plots
|
|
521
|
+
ax.scatter(x1, y1, color="darkturquoise", s=20, alpha=0.4, label="In-control data")
|
|
522
|
+
ax.scatter(x2, y2, color="coral", s=20, alpha=0.4, label="Test data")
|
|
523
|
+
|
|
524
|
+
# Mean lines
|
|
525
|
+
if len(x1):
|
|
526
|
+
ax.plot([x1.min(), x1.max()], [mean_y1, mean_y1],
|
|
527
|
+
linestyle="--", color="darkturquoise", label="In-control mean")
|
|
528
|
+
|
|
529
|
+
if len(x2):
|
|
530
|
+
ax.plot([x2.min(), x2.max()], [mean_y2, mean_y2],
|
|
531
|
+
linestyle="--", color="coral", label="Test mean")
|
|
532
|
+
|
|
533
|
+
# Change-point line
|
|
534
|
+
ax.axvline(x=split, color="grey", linestyle="--", label="Detected change-point")
|
|
535
|
+
|
|
536
|
+
# Labels and title
|
|
537
|
+
ax.set_title("AI model performance versus time", fontsize=16, fontweight="bold")
|
|
538
|
+
ax.set_xlabel("Time", fontsize=14, fontweight="bold")
|
|
539
|
+
ax.set_ylabel("AI model performance", fontsize=14, fontweight="bold")
|
|
540
|
+
|
|
541
|
+
# Tick spacing similar to dtick=20
|
|
542
|
+
ax.set_xticks(np.arange(0, self.total_days, 20))
|
|
543
|
+
|
|
544
|
+
# Background color (optional)
|
|
545
|
+
try:
|
|
546
|
+
ax.set_facecolor(self.config["color"]["blue_005"])
|
|
547
|
+
except Exception:
|
|
548
|
+
pass
|
|
549
|
+
|
|
550
|
+
# Legend (top center like Plotly)
|
|
551
|
+
ax.legend(loc="lower center", bbox_to_anchor=(0.5, 1.15), ncol=3, frameon=True)
|
|
552
|
+
|
|
553
|
+
plt.tight_layout()
|
|
554
|
+
|
|
555
|
+
return fig
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def plot_cusum_chart(self):
|
|
560
|
+
"""
|
|
561
|
+
Plot the CUSUM chart of the performance metric: the positive (S_hi) and negative (S_lo) CUSUM statistics
|
|
562
|
+
along with the decision threshold
|
|
563
|
+
used for change detection.
|
|
564
|
+
|
|
565
|
+
Returns
|
|
566
|
+
-------
|
|
567
|
+
matplotlib.figure.Figure
|
|
568
|
+
Matplotlib figure with the CUSUM chart.
|
|
569
|
+
"""
|
|
570
|
+
|
|
571
|
+
fig, ax = plt.subplots(figsize=(10, 5))
|
|
572
|
+
|
|
573
|
+
# X axis
|
|
574
|
+
x = np.arange(len(self.S_hi))
|
|
575
|
+
|
|
576
|
+
# Plot S_hi and S_lo (normalized)
|
|
577
|
+
ax.plot(x, self.S_hi / self.in_std,
|
|
578
|
+
label="Positive changes (S_hi)",
|
|
579
|
+
color="cyan")
|
|
580
|
+
|
|
581
|
+
ax.plot(x, self.S_lo / self.in_std,
|
|
582
|
+
label="Negative changes (S_lo)",
|
|
583
|
+
color="darkcyan")
|
|
584
|
+
|
|
585
|
+
# Threshold line
|
|
586
|
+
threshold = self.H / self.in_std
|
|
587
|
+
ax.plot([0, len(self.S_lo)],
|
|
588
|
+
[threshold, threshold],
|
|
589
|
+
linestyle="--",
|
|
590
|
+
color="magenta",
|
|
591
|
+
label="Threshold (h)")
|
|
592
|
+
|
|
593
|
+
# Determine split point
|
|
594
|
+
split = self.pre_change_days if self.pre_change_days is not None else self.init_days
|
|
595
|
+
|
|
596
|
+
# Background shading (like Plotly rectangles)
|
|
597
|
+
try:
|
|
598
|
+
ax.axvspan(0, split,
|
|
599
|
+
color=self.config["color"]["blue_005"],
|
|
600
|
+
alpha=0.8)
|
|
601
|
+
|
|
602
|
+
ax.axvspan(split, len(self.S_lo),
|
|
603
|
+
color="rgb(253, 243, 235)",
|
|
604
|
+
alpha=0.8)
|
|
605
|
+
except Exception:
|
|
606
|
+
# fallback if config missing
|
|
607
|
+
ax.axvspan(0, split, color="lightblue", alpha=0.3)
|
|
608
|
+
ax.axvspan(split, len(self.S_lo), color="mistyrose", alpha=0.3)
|
|
609
|
+
|
|
610
|
+
# Labels and title
|
|
611
|
+
ax.set_title("CUSUM Chart", fontsize=16, fontweight="bold")
|
|
612
|
+
ax.set_xlabel("Time", fontsize=14, fontweight="bold")
|
|
613
|
+
ax.set_ylabel("CUSUM value", fontsize=14, fontweight="bold")
|
|
614
|
+
|
|
615
|
+
# Tick spacing similar to dtick=20
|
|
616
|
+
ax.set_xticks(np.arange(0, len(self.S_lo), 20))
|
|
617
|
+
|
|
618
|
+
# Legend (top center)
|
|
619
|
+
ax.legend(loc="lower center", bbox_to_anchor=(0.5, 1.15), ncol=3, frameon=True)
|
|
620
|
+
|
|
621
|
+
plt.tight_layout()
|
|
622
|
+
|
|
623
|
+
return fig
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# paths to output files
|
|
2
|
+
[path_output]
|
|
3
|
+
path_figure = "."
|
|
4
|
+
|
|
5
|
+
# primary colors
|
|
6
|
+
[color]
|
|
7
|
+
blue_005 = "#F3F9FC"
|
|
8
|
+
blue_020 = "#D0E4F0"
|
|
9
|
+
blue_040 = "#A3CAE1"
|
|
10
|
+
blue_060 = "#77B0D2"
|
|
11
|
+
blue_080 = "#4F96C4"
|
|
12
|
+
blue_100 = "#007CBA"
|
|
13
|
+
|
|
14
|
+
# controls
|
|
15
|
+
[control]
|
|
16
|
+
save_figure = "false"
|
|
17
|
+
|
|
18
|
+
# CUSUM parameters
|
|
19
|
+
[CUSUM_params]
|
|
20
|
+
list_ARL_0 = [50, 100, 150, 200, 300, 400, 500, 1000]
|
|
21
|
+
shift_in_mean = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aim-cu
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: AIM-CU: A CUSUM-based tool for AI Monitoring
|
|
5
|
+
Home-page: https://github.com/DIDSR/AIM-CU
|
|
6
|
+
Project-URL: User Inteface, https://huggingface.co/spaces/didsr/AIM-CU
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/x-rst
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: numpy>=1.23
|
|
11
|
+
Requires-Dist: pandas>=1.5
|
|
12
|
+
Requires-Dist: matplotlib>=3.6
|
|
13
|
+
Requires-Dist: rpy2>=3.5
|
|
14
|
+
Requires-Dist: tomli; python_version < "3.11"
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
Dynamic: project-url
|
|
20
|
+
Dynamic: requires-dist
|
|
21
|
+
Dynamic: requires-python
|
|
22
|
+
Dynamic: summary
|
|
23
|
+
|
|
24
|
+
AIM-CU: A CUSUM-based tool for AI Monitoring
|
|
25
|
+
============================================
|
|
26
|
+
Monitoring a clinically deployed AI device to detect performance drift is an essential step to ensure the safety and effectiveness of AI.
|
|
27
|
+
|
|
28
|
+
AIM-CU is a statistical tool for AI monitoring based on a cumulative sum (AIM-CU) approach.
|
|
29
|
+
|
|
30
|
+
AIM-CU computes:
|
|
31
|
+
|
|
32
|
+
* The parameter choices for change-point detection based on an acceptable false alarm rate
|
|
33
|
+
* Detection delay estimates for a given displacement of the performance metric from the target for those parameter choices.
|
|
34
|
+
|
|
35
|
+
System setup
|
|
36
|
+
------------
|
|
37
|
+
Make sure R is installed in the system. There is no specific version that this relies on. Here we have used the version 4.1.2 (2021-11-01).
|
|
38
|
+
Instructions for Linux (the below setup is only performed in Linux):
|
|
39
|
+
|
|
40
|
+
.. code-block:: shell
|
|
41
|
+
|
|
42
|
+
wget -qO- https://cloud.r-project.org/bin/linux/ubuntu/marutter_pubkey.asc | tee -a /etc/apt/trusted.gpg.d/cran_ubuntu_key.asc
|
|
43
|
+
add-apt-repository "deb https://cloud.r-project.org/bin/linux/ubuntu $(lsb_release -cs)-cran40/"
|
|
44
|
+
apt-get install -y --no-install-recommends r-base r-base-dev
|
|
45
|
+
|
|
46
|
+
# setup R configs
|
|
47
|
+
echo "r <- getOption('repos'); r['CRAN'] <- 'http://cran.us.r-project.org'; options(repos = r);" > ~/.Rprofile
|
|
48
|
+
Rscript -e "install.packages('ggplot2')"
|
|
49
|
+
Rscript -e "install.packages('hexbin')"
|
|
50
|
+
Rscript -e "install.packages('lazyeval')"
|
|
51
|
+
Rscript -e "install.packages('cusumcharter')"
|
|
52
|
+
Rscript -e "install.packages('RcppCNPy')"
|
|
53
|
+
Rscript -e "install.packages('spc')"
|
|
54
|
+
|
|
55
|
+
Code execution
|
|
56
|
+
--------------
|
|
57
|
+
Clone AIM-CU repository.
|
|
58
|
+
|
|
59
|
+
.. code-block:: shell
|
|
60
|
+
|
|
61
|
+
git clone https://github.com/DIDSR/AIM-CU.git
|
|
62
|
+
|
|
63
|
+
Run the following commands to install required dependencies (Python = 3.10 is used).
|
|
64
|
+
|
|
65
|
+
.. code-block:: shell
|
|
66
|
+
|
|
67
|
+
apt-get -y install python3
|
|
68
|
+
apt-get -y install pip
|
|
69
|
+
cd AIM-CU
|
|
70
|
+
pip install -r requirements.txt
|
|
71
|
+
|
|
72
|
+
Run AIM-CU.
|
|
73
|
+
|
|
74
|
+
.. code-block:: shell
|
|
75
|
+
|
|
76
|
+
cd src/package
|
|
77
|
+
python3 app.py
|
|
78
|
+
|
|
79
|
+
Open the URL http://0.0.0.0:7860 that is running the AIM-CU locally.
|
|
80
|
+
|
|
81
|
+
Example code execution
|
|
82
|
+
----------------------
|
|
83
|
+
Example code can be run through Jupyter Notebook. Do this by entering the ``jupyter notebook`` command from the ``/src/package/`` directory. The tool is designed to be used through a GUI, not from the console.
|
|
84
|
+
|
|
85
|
+
Demo
|
|
86
|
+
----
|
|
87
|
+
AIM-CU can also be run through the demo available at https://huggingface.co/spaces/didsr/AIM-CU. If Space is paused, click on Restart button. Note: this Space uses a custom Docker container; build may break due to latest package updates pulled by HuggingFace.
|
|
88
|
+
|
|
89
|
+
Usability
|
|
90
|
+
---------
|
|
91
|
+
* Example AI output CSV file is available as `config/spec-60-60.csv <config/spec-60-60.csv>`_ to be uploaded in monitoring phase.
|
|
92
|
+
|
|
93
|
+
* Workflow instruction to run the tool is available at bottom-left of UI.
|
|
94
|
+
|
|
95
|
+
* Sample UI output is available at `assets/ui.png <assets/ui.png>`_.
|
|
96
|
+
|
|
97
|
+
* Setting ``control:save_figure`` to ``true`` from `config.toml <config/config.toml>`_ will save tables and plots in `figure/ <figure/>`_.
|
|
98
|
+
|
|
99
|
+
* Running AIM-CU usually only takes a few seconds, and it does not require a GPU to run.
|
|
100
|
+
|
|
101
|
+
Related References
|
|
102
|
+
------------------
|
|
103
|
+
* Smriti Prathapan, Ravi K. Samala, Nathan Hadjiyski, Pierre‑François D’Haese, Nicholas Petrick, Jana Delfino, Fabien Maldonado, Brandon Nelson, Ghada Zamzmi, Phuong Nguyen, Yelena Yesha, and Berkman Sahiner, "Detecting performance drift in AI models for medical image analysis using CUSUM chart" (Journal in press 2026)
|
|
104
|
+
|
|
105
|
+
* Prathapan, S., Sahiner, B., Kadia, D., and Samala, R.K. 2025, "AIM-CU: A statistical tool for AI Monitoring." (Journal in-press 2026)
|
|
106
|
+
|
|
107
|
+
* Prathapan, S., Samala, R.K., Hadjiyski, N., D’Haese, P.F., Maldonado, F., Nguyen, P., Yesha, Y. and Sahiner, B., 2024, April. Quantifying input data drift in medical machine learning models by detecting change-points in time-series data. In Medical Imaging 2024: Computer-Aided Diagnosis (Vol. 12927, pp. 67-76). SPIE. https://doi.org/10.1117/12.3008771
|
|
108
|
+
|
|
109
|
+
* Smriti Prathapan, Ravi K. Samala, Nathan Hadjiyski, Pierre‑François D’Haese, Nicholas Petrick, Jana Delfino, Fabien Maldonado, Brandon Nelson, Ghada Zamzmi, Phuong Nguyen, Yelena Yesha, and Berkman Sahiner, "Post-market Monitoring of AI-enabled Medical Devices for Radiology and Healthcare Applications" (FDA-UMiami Collaboration Poster, September 2023)
|
|
110
|
+
|
|
111
|
+
Disclaimer
|
|
112
|
+
---------------------
|
|
113
|
+
|
|
114
|
+
**About the Catalog of Regulatory Science Tools**
|
|
115
|
+
|
|
116
|
+
The enclosed tool is *in preparation* for the `Catalog of Regulatory Science Tools <https://cdrh-rst.fda.gov>`_. Note that this software is not (yet) a standalone RST but rather is to be included as a **reference tool** to support other Regulatory Science Tools (such as synthetic datasets) for reproducibility.
|
|
117
|
+
|
|
118
|
+
This catalog collates a variety of regulatory science tools that the FDA's Center for Devices and Radiological Health's (CDRH) Office of Science and Engineering Labs (OSEL) developed. These tools use the most innovative science to support medical device development and patient access to safe and effective medical devices. If you are considering using a tool from this catalog in your marketing submissions, note that these tools have not been qualified as `Medical Device Development Tools <https://www.fda.gov/medical-devices/medical-device-development-tools-mddt>`_ and the FDA has not evaluated the suitability of these tools within any specific context of use. You may `request feedback or meetings for medical device submissions <https://www.fda.gov/regulatory-information/search-fda-guidance-documents/requests-feedback-and-meetings-medical-device-submissions-q-submission-program>`_ as part of the Q-Submission Program.
|
|
119
|
+
|
|
120
|
+
For more information about the Catalog of Regulatory Science Tools, email RST_CDRH@fda.hhs.gov.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
setup.py
|
|
3
|
+
aim_cu/README.rst
|
|
4
|
+
aim_cu/SECURITY.md
|
|
5
|
+
aim_cu/__init__.py
|
|
6
|
+
aim_cu/aim_cu.py
|
|
7
|
+
aim_cu/config.toml
|
|
8
|
+
aim_cu.egg-info/PKG-INFO
|
|
9
|
+
aim_cu.egg-info/SOURCES.txt
|
|
10
|
+
aim_cu.egg-info/dependency_links.txt
|
|
11
|
+
aim_cu.egg-info/requires.txt
|
|
12
|
+
aim_cu.egg-info/top_level.txt
|
|
13
|
+
test/test_aim_cu.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aim_cu
|
aim_cu-0.2.0/setup.cfg
ADDED
aim_cu-0.2.0/setup.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
this_dir = Path(__file__).parent
|
|
5
|
+
long_description = (this_dir / "aim_cu" / "README.rst").read_text(encoding="utf-8")
|
|
6
|
+
|
|
7
|
+
setup(
|
|
8
|
+
name="aim-cu",
|
|
9
|
+
version="0.2.0",
|
|
10
|
+
description="AIM-CU: A CUSUM-based tool for AI Monitoring",
|
|
11
|
+
long_description=long_description,
|
|
12
|
+
long_description_content_type="text/x-rst",
|
|
13
|
+
authors="Smriti Prathapan, Berkman Sahiner, Dhaval Kadia and Ravi Samala",
|
|
14
|
+
packages=find_packages(),
|
|
15
|
+
python_requires=">=3.9",
|
|
16
|
+
install_requires=[
|
|
17
|
+
"numpy>=1.23",
|
|
18
|
+
"pandas>=1.5",
|
|
19
|
+
"matplotlib>=3.6",
|
|
20
|
+
"rpy2>=3.5",
|
|
21
|
+
"tomli; python_version<'3.11'",
|
|
22
|
+
],
|
|
23
|
+
include_package_data=True,
|
|
24
|
+
package_data={
|
|
25
|
+
"aim_cu": ["config.toml", "SECURITY.md", "README.rst"],
|
|
26
|
+
},
|
|
27
|
+
url="https://github.com/DIDSR/AIM-CU",
|
|
28
|
+
project_urls={
|
|
29
|
+
"User Inteface" : "https://huggingface.co/spaces/didsr/AIM-CU",
|
|
30
|
+
},
|
|
31
|
+
)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#Usage:
|
|
2
|
+
# python test_aim_cu.py --csv /scratch/smriti.prathapan/fromOHPC/note1/AIM-CU-package/spec-60-60.csv --norm_h 5
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
# import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import warnings
|
|
8
|
+
warnings.simplefilter("ignore")
|
|
9
|
+
import aim_cu
|
|
10
|
+
# import matplotlib
|
|
11
|
+
# from collections import OrderedDict
|
|
12
|
+
# matplotlib.use("Agg")
|
|
13
|
+
# import matplotlib.pyplot as plt
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
parser = argparse.ArgumentParser()
|
|
17
|
+
parser.add_argument("--csv", default=None, help="input the CSV path")
|
|
18
|
+
parser.add_argument("--init_days", type=int, default=30)
|
|
19
|
+
parser.add_argument("--norm_k", type=float, default=0.5)
|
|
20
|
+
parser.add_argument("--norm_h", type=float, default=4.0)
|
|
21
|
+
#parser.add_argument("--show_plots", action="store_true")
|
|
22
|
+
args = parser.parse_args()
|
|
23
|
+
|
|
24
|
+
# --------------------------------------------------------
|
|
25
|
+
# I Compute CUSUM parameters and average run length
|
|
26
|
+
# --------------------------------------------------------
|
|
27
|
+
h = float(args.norm_h)
|
|
28
|
+
print("\n=== SPC helper functions for computing CUSUM parameters ===")
|
|
29
|
+
k_for_arl100 = aim_cu.get_ref_value(h=h, ARL_0=100.0)
|
|
30
|
+
#print(f"k (for h={h}, ARL0=100): {k_for_arl100}")
|
|
31
|
+
|
|
32
|
+
h_back = aim_cu.get_threshold(k=k_for_arl100, ARL_0=100.0)
|
|
33
|
+
print(f"h (for k={k_for_arl100}, ARL0=100): {h_back}")
|
|
34
|
+
|
|
35
|
+
df_k, dict_k = aim_cu.get_ref_values(h=4.0, list_ARL_0=[50.0, 100.0, 200.0])
|
|
36
|
+
print(f"list of k values h={h} : \n")
|
|
37
|
+
print(df_k)
|
|
38
|
+
|
|
39
|
+
#print(aim_cu.shift_in_mean)
|
|
40
|
+
#print(aim_cu.dict_ARL0_k)
|
|
41
|
+
|
|
42
|
+
ARL_1 = aim_cu.compute_ARL1(h=h_back,k=k_for_arl100, mu1=0.65)
|
|
43
|
+
print(f"ARL_1 : {ARL_1}")
|
|
44
|
+
|
|
45
|
+
df_arl_1 = aim_cu.compute_ARL1_table(h=4, shift_in_mean=[0.1, 0.2, 0.3], dict_ARL0_k=dict_k)
|
|
46
|
+
print(f"ARL_1 table : {df_arl_1}")
|
|
47
|
+
|
|
48
|
+
# -------------------------------
|
|
49
|
+
# II Performance drift detection
|
|
50
|
+
# -------------------------------
|
|
51
|
+
print("\n=== Performance drift detection using CUSUM ===")
|
|
52
|
+
cusum = aim_cu.CUSUM()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# If config.toml is not found, initialize() will sys.exit(1)
|
|
56
|
+
try:
|
|
57
|
+
cusum.initialize()
|
|
58
|
+
except SystemExit:
|
|
59
|
+
print("WARNING: config.toml not found. Using fallback config.")
|
|
60
|
+
cusum.config = {
|
|
61
|
+
"color": {"blue_005": "white"},
|
|
62
|
+
"control": {"save_figure": "false"},
|
|
63
|
+
"path_output": {"path_figure": "."},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if args.csv:
|
|
67
|
+
df = pd.read_csv(args.csv)
|
|
68
|
+
cusum.set_df_metric_csv(df)
|
|
69
|
+
else:
|
|
70
|
+
cusum.set_df_metric_default()
|
|
71
|
+
|
|
72
|
+
cusum.set_init_stats(args.init_days)
|
|
73
|
+
print(f"Total observations: {cusum.total_days}")
|
|
74
|
+
print(f"Baseline observations: {cusum.init_days}")
|
|
75
|
+
|
|
76
|
+
cusum.change_detection(normalized_ref_value=args.norm_k, normalized_threshold=args.norm_h)
|
|
77
|
+
|
|
78
|
+
fig1 = cusum.plot_input_data()
|
|
79
|
+
#fig1.write_image("input_data.png")
|
|
80
|
+
fig1.savefig("input_data.png", dpi=200, bbox_inches="tight")
|
|
81
|
+
|
|
82
|
+
fig2 = cusum.plot_changepoint()
|
|
83
|
+
fig2.savefig("changepoint.png", dpi=200, bbox_inches="tight")
|
|
84
|
+
|
|
85
|
+
fig3 = cusum.plot_cusum_chart()
|
|
86
|
+
fig3.savefig("cusum_chart.png", dpi=200, bbox_inches="tight")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
main()
|