Repository: nilearn/nilearn

Issue:
nistats: dependence between function calls – strange bug may affect reproducibility
Hi,

I've encountered very strange behavior of two functions – `map_threshold` from `nistats.thresholding` module and `get_clusters_table` from `nistats.reporting` module. It seems like the threshold value returned by `map_threshold` depends on the last output value(s) of `get_clusters_table` function or some internal function variables. Impact seems to be severe (the higher the `cluster_threshold` value passed to `get_cluster_table`, the higher the threshold returned by subsequent call of `map_threshold`). 

Here is a simple demonstration: https://github.com/kbonna/decidenet/blob/master/activation_analysis/nistats_map_threshold_bug.ipynb

I've tried to find a source of the problem studying code of both functions, but I everything seems to be ok at first glance. 

Am I missing something simple?


Discussion:
thanks for reporting. there seems to be a bug in `get_clusters_table`, both in [nilearn](https://github.com/nilearn/nilearn/blob/4c1c7337b38506e6ef60873cfe3dd316ce869cf0/nilearn/reporting/_get_clusters_table.py#L146) and [nistats](https://github.com/nilearn/nistats/blob/bd4e27fdd5487b1ff920d4ae2473a7671153d2b3/nistats/reporting/_get_clusters_table.py#L145) : the input image is modified
(in both the problematic line is `            stat_map[label_map == c_val] = 0` ), to reproduce the problem:
```
import numpy as np
from nilearn import datasets, image
from nilearn.reporting import get_clusters_table
# from nistats.reporting._get_clusters_table import get_clusters_table

img = image.load_img(datasets.fetch_neurovault_motor_task()["images"][0])
data = image.get_data(img).copy()
assert np.allclose(data, image.get_data(img)), "???"
get_clusters_table(img, 2., cluster_threshold=5)
assert np.allclose(data, image.get_data(img)), "image was modified"
```

would you like to open a PR to fix this issue?
Yes, I will try to fix that and submit a PR

Resolving patch:
diff --git a/nilearn/reporting/_get_clusters_table.py b/nilearn/reporting/_get_clusters_table.py
--- a/nilearn/reporting/_get_clusters_table.py
+++ b/nilearn/reporting/_get_clusters_table.py
@@ -120,7 +120,14 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None,
         rather than any peaks/subpeaks.
     """
     cols = ['Cluster ID', 'X', 'Y', 'Z', 'Peak Stat', 'Cluster Size (mm3)']
-    stat_map = get_data(stat_img)
+
+    # If cluster threshold is used, there is chance that stat_map will be
+    # modified, therefore copy is needed
+    if cluster_threshold is None:
+        stat_map = get_data(stat_img)
+    else:
+        stat_map = get_data(stat_img).copy()
+
     conn_mat = np.zeros((3, 3, 3), int)  # 6-connectivity, aka NN1 or "faces"
     conn_mat[1, 1, :] = 1
     conn_mat[1, :, 1] = 1


Test patch:
diff --git a/nilearn/reporting/tests/test_reporting.py b/nilearn/reporting/tests/test_reporting.py
--- a/nilearn/reporting/tests/test_reporting.py
+++ b/nilearn/reporting/tests/test_reporting.py
@@ -15,6 +15,7 @@
                                plot_event,
                                plot_design_matrix,
                                )
+from nilearn.image import get_data
 from nilearn.reporting._get_clusters_table import _local_max
 
 # Avoid making pyflakes unhappy
@@ -139,3 +140,30 @@ def test_get_clusters_table():
     # test empty table on high cluster threshold
     cluster_table = get_clusters_table(stat_img, 4, 9)
     assert len(cluster_table) == 0
+
+
+def test_get_clusters_table_not_modifying_stat_image():
+    shape = (9, 10, 11)
+    data = np.zeros(shape)
+    data[2:4, 5:7, 6:8] = 5.
+    data[0:3, 0:3, 0:3] = 6.
+
+    stat_img = nib.Nifti1Image(data, np.eye(4))
+    data_orig = get_data(stat_img).copy()
+
+    # test one cluster should be removed
+    clusters_table = get_clusters_table(stat_img, 4, cluster_threshold=10)
+    assert np.allclose(data_orig, get_data(stat_img))
+    assert len(clusters_table) == 1
+
+    # test no clusters should be removed
+    stat_img = nib.Nifti1Image(data, np.eye(4))
+    clusters_table = get_clusters_table(stat_img, 4, cluster_threshold=7)
+    assert np.allclose(data_orig, get_data(stat_img))
+    assert len(clusters_table) == 2
+
+    # test cluster threshold is None
+    stat_img = nib.Nifti1Image(data, np.eye(4))
+    clusters_table = get_clusters_table(stat_img, 4, cluster_threshold=None)
+    assert np.allclose(data_orig, get_data(stat_img))
+    assert len(clusters_table) == 2