├── .github └── workflows │ ├── auto_deploy_docs_pypi_onrelease.yml │ ├── auto_formatting_tests_and_coverage.yml │ ├── manual_deploy_docs_only.yml │ └── manual_deploy_pypi_only.yml ├── .gitignore ├── .vscode └── extensions.json ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── _templates │ └── layout.html ├── api.rst ├── conf.py ├── index.rst └── install.rst ├── examples ├── 01_DataOperations │ ├── README.txt │ ├── plot_adjacency.py │ ├── plot_design_matrix.py │ ├── plot_download.py │ ├── plot_mask.py │ ├── plot_mni_prefs.py │ └── plot_neurovault_io.py ├── 02_Analysis │ ├── README.txt │ ├── plot_decomposition.py │ ├── plot_hyperalignment.py │ ├── plot_multivariate_classification.py │ ├── plot_multivariate_prediction.py │ ├── plot_similarity_example.py │ └── plot_univariate_regression.py └── README.txt ├── nltools ├── __init__.py ├── analysis.py ├── cross_validation.py ├── data │ ├── __init__.py │ ├── adjacency.py │ ├── brain_data.py │ └── design_matrix.py ├── datasets.py ├── external │ ├── __init__.py │ ├── hrf.py │ └── srm.py ├── file_reader.py ├── mask.py ├── plotting.py ├── prefs.py ├── resources │ ├── MNI152_T1_2mm.nii.gz │ ├── MNI152_T1_2mm_brain.nii.gz │ ├── MNI152_T1_2mm_brain_mask.nii.gz │ ├── MNI152_T1_2mm_brain_mask_no_ventricles.nii.gz │ ├── MNI152_T1_3mm.nii.gz │ ├── MNI152_T1_3mm_brain.nii.gz │ ├── MNI152_T1_3mm_brain_mask.nii.gz │ ├── MNI152_T1_3mm_brain_mask_no_ventricles.nii.gz │ ├── covariates_example.csv │ ├── gm_mask_2mm.nii.gz │ ├── gm_mask_3mm.nii.gz │ ├── onsets_example.txt │ └── onsets_example_with_dur.txt ├── simulator.py ├── stats.py ├── tests │ ├── conftest.py │ ├── matplotlibrc │ ├── new_brain.h5 │ ├── new_double.h5 │ ├── new_single.h5 │ ├── old_brain.h5 │ ├── old_double.h5 │ ├── old_single.h5 │ ├── test_adjacency.py │ ├── test_analysis.py │ ├── test_brain_data.py │ ├── test_cross_validation.py │ ├── test_design_matrix.py │ ├── test_file_reader.py │ ├── test_groupby.py │ ├── test_mask.py │ ├── test_prefs.py │ ├── test_simulator.py │ ├── test_stats.py │ └── test_utils.py ├── utils.py └── version.py ├── optional-dependencies.txt ├── pytest.ini ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg └── setup.py /.github/workflows/auto_deploy_docs_pypi_onrelease.yml: -------------------------------------------------------------------------------- 1 | name: (Auto-On-Release) Deploy Docs and PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | # Job (1): Build and deploy docs. 9 | docs: 10 | name: Build docs and auto-examples 11 | runs-on: ubuntu-latest 12 | defaults: 13 | run: 14 | shell: bash -l {0} 15 | steps: 16 | - name: Download and setup Miniconda 17 | uses: conda-incubator/setup-miniconda@v3 18 | with: 19 | miniconda-version: "latest" 20 | python-version: 3.8 21 | 22 | - name: Checkout Code 23 | uses: actions/checkout@v2 24 | 25 | - name: Install Dependencies 26 | run: | 27 | conda activate test 28 | conda env list 29 | pip install -r requirements-dev.txt 30 | pip install -r optional-dependencies.txt 31 | 32 | - name: Build docs 33 | run: | 34 | cd docs 35 | make clean 36 | make html 37 | touch _build/html/.nojekyll 38 | echo 'nltools.org' > _build/html/CNAME 39 | 40 | - name: Deploy docs 41 | if: success() 42 | uses: crazy-max/ghaction-github-pages@v2 43 | with: 44 | target_branch: gh-pages 45 | build_dir: docs/_build/html 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | 49 | # Job (2): Build package and upload to pypi 50 | deploy: 51 | name: Build & deploy package 52 | runs-on: ubuntu-latest 53 | steps: 54 | - name: Checkout Code 55 | uses: actions/checkout@v2 56 | 57 | - name: Setup Python 58 | uses: actions/setup-python@v2 59 | with: 60 | python-version: "3.8" 61 | 62 | - name: Pypa build 63 | run: | 64 | python3 -m pip install build --user 65 | 66 | - name: Wheel and source build 67 | run: | 68 | python3 -m build --sdist --wheel --outdir dist/ 69 | 70 | - name: Publish to PyPI 71 | uses: pypa/gh-action-pypi-publish@release/v1 72 | with: 73 | password: ${{ secrets.PYPI_API_TOKEN }} 74 | -------------------------------------------------------------------------------- /.github/workflows/auto_formatting_tests_and_coverage.yml: -------------------------------------------------------------------------------- 1 | name: (Auto) Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | 9 | pull_request: 10 | branches: 11 | - main 12 | - master 13 | 14 | # Run tests every week on sundays 15 | schedule: 16 | - cron: "0 0 * * 0" 17 | 18 | jobs: 19 | # Job (1): Run testing in parallel against multiples OSs and Python versions 20 | test: 21 | name: Test 22 | runs-on: ${{ matrix.os }} 23 | # Determines whether the entire workflow should pass/fail based on parallel jobs 24 | continue-on-error: ${{ matrix.experimental }} 25 | defaults: 26 | # This ensures each step gets properly configured bash shell for conda commands to work 27 | run: 28 | shell: bash -l {0} 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | # OSs to test 33 | os: [ubuntu-latest, macos-latest] 34 | # Python versions to test 35 | python-version: [3.8, 3.9, '3.10'] 36 | # By default everything should pass for the workflow to pass 37 | experimental: [false] 38 | include: 39 | # Windows sometimes fails to install due to dependency changes, but eventually sort themselves out. So let these tests fail. Also issue on macos 3.11 with joblib so let that fail 40 | - os: windows-latest 41 | python-version: 3.8 42 | experimental: true 43 | - os: windows-latest 44 | python-version: 3.9 45 | experimental: true 46 | - os: windows-latest 47 | python-version: '3.10' 48 | experimental: true 49 | - os: windows-latest 50 | python-version: 3.11 51 | experimental: true 52 | - os: macos-latest 53 | python-version: 3.11 54 | experimental: true 55 | - os: ubuntu-latest 56 | python-version: 3.11 57 | experimental: false 58 | - os: macos-14 59 | python-version: 3.8 60 | experimental: true 61 | - os: macos-14 62 | python-version: 3.9 63 | experimental: true 64 | - os: macos-14 65 | python-version: '3.10' 66 | experimental: true 67 | - os: macos-14 68 | python-version: 3.11 69 | experimental: true 70 | steps: 71 | # Step up miniconda 72 | - name: Download and setup Miniconda 73 | uses: conda-incubator/setup-miniconda@v3 74 | with: 75 | miniconda-version: "latest" 76 | python-version: ${{ matrix.python-version }} 77 | 78 | # Check out latest code on github 79 | - name: Checkout Code 80 | uses: actions/checkout@v2 81 | 82 | # Install common sci-py packages via conda as well as testing packages and requirements 83 | - name: Install Dependencies 84 | run: | 85 | conda activate test 86 | conda env list 87 | pip install -r requirements-dev.txt 88 | pip install -r optional-dependencies.txt 89 | 90 | # Check code formatting 91 | - name: Check code formatting 92 | run: | 93 | conda activate test 94 | black --version 95 | black --check --diff --verbose nltools 96 | 97 | # Actually run the tests with coverage 98 | - name: Run Tests 99 | run: | 100 | conda activate test 101 | coverage run --source=nltools -m pytest -rs 102 | 103 | # Send coverage to coveralls.io but waiting on parallelization to finish 104 | # Not using the official github action in the marketplace to upload because it requires a .lcov file, which pytest doesn't generate. It's just easier to use the coveralls python library which does the same thing, but works with pytest. 105 | - name: Upload Coverage 106 | # The coveralls python package has some 422 server issues with uploads from github-actions so try both service providers, for more see: 107 | # https://github.com/TheKevJames/coveralls-python/issues/252 108 | run: coveralls --service=github || coveralls --service=github-actions 109 | env: 110 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 111 | COVERALLS_FLAG_NAME: $${{ matrix}} 112 | COVERALLS_PARALLEL: true 113 | 114 | # Job (2): Send a finish notification to coveralls.io to integrate coverage across parallel tests 115 | coveralls: 116 | name: Coveralls.io Upload 117 | needs: test 118 | runs-on: ubuntu-latest 119 | container: python:3-slim 120 | continue-on-error: true 121 | steps: 122 | - name: Finished 123 | run: | 124 | pip3 install --upgrade coveralls 125 | coveralls --service=github --finish || coveralls --service=github-actions --finish 126 | env: 127 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 128 | 129 | # Job (3): Build docs, but don't deploy. This is effectively another layer of testing because of our sphinx-gallery auto-examples 130 | docs: 131 | name: Build docs and auto-examples 132 | runs-on: ubuntu-latest 133 | #TODO: Remove when upstream doc building issues are resolved 134 | continue-on-error: true 135 | defaults: 136 | run: 137 | shell: bash -l {0} 138 | steps: 139 | - name: Download and setup Miniconda 140 | uses: conda-incubator/setup-miniconda@v3 141 | with: 142 | miniconda-version: "latest" 143 | python-version: 3.8 144 | 145 | - name: Checkout Code 146 | uses: actions/checkout@v2 147 | 148 | - name: Install Dependencies 149 | run: | 150 | conda activate test 151 | conda env list 152 | pip install -r requirements-dev.txt 153 | pip install -r optional-dependencies.txt 154 | 155 | - name: Build docs 156 | run: | 157 | cd docs 158 | make clean 159 | make html 160 | -------------------------------------------------------------------------------- /.github/workflows/manual_deploy_docs_only.yml: -------------------------------------------------------------------------------- 1 | name: (Manual) Deploy Docs 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | docs: 7 | name: Build docs and auto-examples 8 | runs-on: ubuntu-latest 9 | defaults: 10 | run: 11 | shell: bash -l {0} 12 | steps: 13 | - name: Download and setup Miniconda 14 | uses: conda-incubator/setup-miniconda@v3 15 | with: 16 | miniconda-version: "latest" 17 | python-version: 3.8 18 | 19 | - name: Checkout Code 20 | uses: actions/checkout@v2 21 | 22 | - name: Install Dependencies 23 | run: | 24 | conda activate test 25 | conda env list 26 | pip install -r requirements-dev.txt 27 | pip install -r optional-dependencies.txt 28 | 29 | - name: Build docs 30 | run: | 31 | cd docs 32 | make clean 33 | make html 34 | touch _build/html/.nojekyll 35 | echo 'nltools.org' > _build/html/CNAME 36 | 37 | - name: Deploy docs 38 | if: success() 39 | uses: crazy-max/ghaction-github-pages@v2 40 | with: 41 | target_branch: gh-pages 42 | build_dir: docs/_build/html 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | -------------------------------------------------------------------------------- /.github/workflows/manual_deploy_pypi_only.yml: -------------------------------------------------------------------------------- 1 | name: (Manual) Deploy PyPi 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | deploy: 7 | name: Build & deploy package 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout Code 11 | uses: actions/checkout@v2 12 | 13 | - name: Setup Python 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: "3.8" 17 | 18 | - name: Pypa build 19 | run: | 20 | python3 -m pip install build --user 21 | 22 | - name: Wheel and source build 23 | run: | 24 | python3 -m build --sdist --wheel --outdir dist/ 25 | 26 | - name: Publish to PyPI 27 | uses: pypa/gh-action-pypi-publish@release/v1 28 | with: 29 | password: ${{ secrets.PYPI_API_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python Related # 2 | ################### 3 | *.pyc 4 | docs/_build 5 | docs/auto_examples 6 | *.log 7 | *.egg* 8 | build/ 9 | dist/ 10 | .cache/ 11 | htmlcov 12 | .pytest_cache/* 13 | dev/ 14 | # Logs and databases # 15 | ###################### 16 | *.log 17 | *.sql 18 | *.sqlite 19 | 20 | # iPython Notebook Caches # 21 | ########################### 22 | scripts/nilearn_cache/ 23 | .ipynb_checkpoints 24 | 25 | # OS generated files # 26 | ###################### 27 | .DS_Store 28 | .DS_Store? 29 | ._* 30 | .Spotlight-V100 31 | .Trashes 32 | thumbs.db 33 | Thumbs.db 34 | 35 | # Tests & Coverage 36 | ################## 37 | .coverage 38 | htmlcov/ 39 | .pytest_cache 40 | 41 | # PyCharm 42 | ######### 43 | .idea 44 | 45 | # TOX 46 | ##### 47 | .tox 48 | .tox/* 49 | 50 | # Docs 51 | ###### 52 | examples/**/*.nii.gz 53 | data.nii.gz 54 | docs/backreferences/ 55 | environment.yml 56 | rep_id.csv 57 | y.csv 58 | env 59 | .vscode/settings.json 60 | .vscode/bookmarks.json 61 | dev.py 62 | dev.ipynb -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "kevinrose.vsc-python-indent", 4 | "njpwerner.autodocstring" 5 | ] 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2018 Cosan Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | recursive-include nltools/resources * 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Package versioning](https://img.shields.io/pypi/v/nltools.svg)](https://pypi.org/project/nltools/) 2 | [![(Auto-On-Push/PR) Formatting, Tests, and Coverage](https://github.com/cosanlab/nltools/actions/workflows/auto_formatting_tests_and_coverage.yml/badge.svg)](https://github.com/cosanlab/nltools/actions/workflows/auto_formatting_tests_and_coverage.yml) 3 | [![codecov](https://codecov.io/gh/cosanlab/nltools/branch/master/graph/badge.svg)](https://codecov.io/gh/cosanlab/nltools) 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/625677967a0749299f38c2bf8ee269c3)](https://www.codacy.com/app/ljchang/nltools?utm_source=github.com&utm_medium=referral&utm_content=ljchang/nltools&utm_campaign=Badge_Grade) 5 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.2229813.svg)](https://doi.org/10.5281/zenodo.2229813) 6 | ![Python Versions](https://img.shields.io/badge/python-3.7%20%7C%203.8-blue) 7 | ![Platforms](https://img.shields.io/badge/platform-linux%20%7C%20osx%20%7C%20win-blue) 8 | 9 | # NLTools 10 | 11 | Python toolbox for analyzing neuroimaging data. It is particularly useful for conducting multivariate analyses. It is originally based on Tor Wager's object oriented matlab [canlab core tools](http://wagerlab.colorado.edu/tools) and relies heavily on [nilearn](http://nilearn.github.io) and [scikit learn](http://scikit-learn.org/stable/index.html). Nltools is only compatible with Python 3.7+. 12 | 13 | ## Documentation 14 | 15 | Documentation and tutorials are available at https://nltools.org 16 | 17 | ## Installation 18 | 19 | 1. Method 1 (stable) 20 | 21 | ``` 22 | pip install nltools 23 | ``` 24 | 25 | 2. Method 2 (bleeding edge) 26 | 27 | ``` 28 | pip install git+https://github.com/cosanlab/nltools 29 | ``` 30 | 31 | 3. Method 3 (for development) 32 | 33 | ``` 34 | git clone https://github.com/cosanlab/nltools 35 | pip install -e nltools 36 | ``` 37 | 38 | ## Preprocessing 39 | 40 | Nltools has minimal routines for pre-processing data. For more complete pre-processing pipelines please see our [cosanlab_preproc](https://github.com/cosanlab/cosanlab_preproc) library built with `nipype`. 41 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | rm -rf auto_examples/ 54 | rm -rf modules/generated/* 55 | 56 | html: 57 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 58 | @echo 59 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 60 | 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | singlehtml: 67 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 68 | @echo 69 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 70 | 71 | pickle: 72 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 73 | @echo 74 | @echo "Build finished; now you can process the pickle files." 75 | 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | htmlhelp: 82 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 83 | @echo 84 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 85 | ".hhp project file in $(BUILDDIR)/htmlhelp." 86 | 87 | qthelp: 88 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 89 | @echo 90 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 91 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 92 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/neurolearn.qhcp" 93 | @echo "To view the help file:" 94 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/neurolearn.qhc" 95 | 96 | applehelp: 97 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 98 | @echo 99 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 100 | @echo "N.B. You won't be able to view it unless you put it in" \ 101 | "~/Library/Documentation/Help or install it in your application" \ 102 | "bundle." 103 | 104 | devhelp: 105 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 106 | @echo 107 | @echo "Build finished." 108 | @echo "To view the help file:" 109 | @echo "# mkdir -p $$HOME/.local/share/devhelp/neurolearn" 110 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/neurolearn" 111 | @echo "# devhelp" 112 | 113 | epub: 114 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 115 | @echo 116 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 117 | 118 | latex: 119 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 120 | @echo 121 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 122 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 123 | "(use \`make latexpdf' here to do that automatically)." 124 | 125 | latexpdf: 126 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 127 | @echo "Running LaTeX files through pdflatex..." 128 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 129 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 130 | 131 | latexpdfja: 132 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 133 | @echo "Running LaTeX files through platex and dvipdfmx..." 134 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 135 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 136 | 137 | text: 138 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 139 | @echo 140 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 141 | 142 | man: 143 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 144 | @echo 145 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 146 | 147 | texinfo: 148 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 149 | @echo 150 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 151 | @echo "Run \`make' in that directory to run these through makeinfo" \ 152 | "(use \`make info' here to do that automatically)." 153 | 154 | info: 155 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 156 | @echo "Running Texinfo files through makeinfo..." 157 | make -C $(BUILDDIR)/texinfo info 158 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 159 | 160 | gettext: 161 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 162 | @echo 163 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 164 | 165 | changes: 166 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 167 | @echo 168 | @echo "The overview file is in $(BUILDDIR)/changes." 169 | 170 | linkcheck: 171 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 172 | @echo 173 | @echo "Link check complete; look for any errors in the above output " \ 174 | "or in $(BUILDDIR)/linkcheck/output.txt." 175 | 176 | doctest: 177 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 178 | @echo "Testing of doctests in the sources finished, look at the " \ 179 | "results in $(BUILDDIR)/doctest/output.txt." 180 | 181 | coverage: 182 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 183 | @echo "Testing of coverage in the sources finished, look at the " \ 184 | "results in $(BUILDDIR)/coverage/python.txt." 185 | 186 | xml: 187 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 188 | @echo 189 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 190 | 191 | pseudoxml: 192 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 193 | @echo 194 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 195 | 196 | html-noplot: 197 | $(SPHINXBUILD) -D plot_gallery=0 -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 198 | @echo 199 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 200 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} {% block extrahead %} {{ super() }} 2 | 3 | 27 | 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. _api_ref: 4 | 5 | API Reference 6 | ************* 7 | 8 | This reference provides detailed documentation for all modules, classes, and 9 | methods in the current release of Neurolearn. 10 | 11 | 12 | :mod:`nltools.data`: Data Types 13 | =============================== 14 | 15 | .. autoclass:: nltools.data.Brain_Data 16 | :members: 17 | 18 | .. autoclass:: nltools.data.Adjacency 19 | :members: 20 | 21 | .. autoclass:: nltools.data.Groupby 22 | :members: 23 | 24 | .. autoclass:: nltools.data.Design_Matrix 25 | :members: 26 | 27 | :mod:`nltools.analysis`: Analysis Tools 28 | ======================================= 29 | 30 | .. autoclass:: nltools.analysis.Roc 31 | :members: 32 | 33 | :mod:`nltools.stats`: Stats Tools 34 | ================================= 35 | 36 | .. automodule:: nltools.stats 37 | :members: 38 | 39 | :mod:`nltools.datasets`: Dataset Tools 40 | ====================================== 41 | 42 | .. automodule:: nltools.datasets 43 | :members: 44 | 45 | :mod:`nltools.cross_validation`: Cross-Validation Tools 46 | ======================================================= 47 | 48 | .. automodule:: nltools.cross_validation 49 | :members: 50 | 51 | .. autoclass:: nltools.cross_validation.KFoldStratified 52 | :members: 53 | 54 | :mod:`nltools.mask`: Mask Tools 55 | =============================== 56 | 57 | .. automodule:: nltools.mask 58 | :members: 59 | 60 | :mod:`nltools.file_reader`: File Reading 61 | ======================================== 62 | 63 | .. automodule:: nltools.file_reader 64 | :members: 65 | 66 | :mod:`nltools.utils`: Utilities 67 | =============================== 68 | 69 | .. automodule:: nltools.utils 70 | :members: 71 | 72 | :mod:`nltools.prefs`: Preferences 73 | ================================= 74 | 75 | This module can be used to adjust the default MNI template settings that are used 76 | internally by all `Brain_Data` operations. By default all operations are performed in 77 | **MNI152 2mm space**. Thus any files loaded with be resampled to this space by default.You can control this on a per-file loading basis using the `mask` argument of `Brain_Data`, e.g. 78 | 79 | .. code-block:: 80 | 81 | from nltools.data import Brain_Data 82 | 83 | # my_brain will be resampled to 2mm 84 | brain = Brain_Data('my_brain.nii.gz') 85 | 86 | # my_brain will now be resampled to the same space as my_mask 87 | brain = Brain_Data('my_brain.nii.gz', mask='my_mask.nii.gz') # will be resampled 88 | 89 | Alternatively this module can be used to switch between 2mm or 3mm MNI spaces with and without ventricles: 90 | 91 | .. code-block:: 92 | 93 | from nltools.prefs import MNI_Template, resolve_mni_path 94 | from nltools.data import Brain_Data 95 | 96 | # Update the resolution globally 97 | MNI_Template['resolution'] = '3mm' 98 | 99 | # This works too: 100 | MNI_Template.resolution = 3 101 | 102 | # my_brain will be resampled to 3mm and future operation will be in 3mm space 103 | brain = Brain_Data('my_brain.nii.gz') 104 | 105 | # get the template nifti files 106 | resolve_mni_path(MNI_Template) 107 | 108 | # will print like: 109 | { 110 | 'resolution': '3mm', 111 | 'mask_type': 'with_ventricles', 112 | 'mask': '/Users/Esh/Documents/pypackages/nltools/nltools/resources/MNI152_T1_3mm_brain_mask.nii.gz', 113 | 'plot': '/Users/Esh/Documents/pypackages/nltools/nltools/resources/MNI152_T1_3mm.nii.gz', 114 | 'brain': 115 | '/Users/Esh/Documents/pypackages/nltools/nltools/resources/MNI152_T1_3mm_brain.nii.gz' 116 | } 117 | 118 | .. automodule:: nltools.prefs 119 | :members: 120 | :show-inheritance: 121 | 122 | :mod:`nltools.plotting`: Plotting Tools 123 | ======================================= 124 | 125 | .. automodule:: nltools.plotting 126 | :members: 127 | 128 | :mod:`nltools.simulator`: Simulator Tools 129 | ========================================= 130 | 131 | .. automodule:: nltools.simulator 132 | :members: 133 | 134 | .. autoclass:: nltools.simulator.Simulator 135 | :members: 136 | 137 | 138 | Index 139 | ===== 140 | 141 | * :ref:`genindex` 142 | * :ref:`modindex` 143 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # nltools documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Jun 4 07:22:28 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import shlex 18 | import time 19 | 20 | sys.path.insert(0, os.path.abspath("sphinxext")) 21 | import sphinx_gallery 22 | import sphinx_bootstrap_theme 23 | 24 | # If extensions (or modules to document with autodoc) are in another directory, 25 | # add these directories to sys.path here. If the directory is relative to the 26 | # documentation root, use os.path.abspath to make it absolute, like shown here. 27 | sys.path.insert(0, os.path.abspath("../")) 28 | 29 | version = {} 30 | with open("../nltools/version.py") as f: 31 | exec(f.read(), version) 32 | version = version["__version__"] 33 | 34 | # -- General configuration ------------------------------------------------ 35 | 36 | # If your documentation needs a minimal Sphinx version, state it here. 37 | # needs_sphinx = '3.2.1' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | "sphinx.ext.autodoc", 44 | "sphinx.ext.autosummary", 45 | "sphinx.ext.viewcode", 46 | "sphinx.ext.mathjax", 47 | "sphinx_gallery.gen_gallery", 48 | ] 49 | 50 | # Add any paths that contain templates here, relative to this directory. 51 | templates_path = ["_templates"] 52 | 53 | # Paths for sphinx gallery auto generated examples 54 | sphinx_gallery_conf = { 55 | # path to your examples scripts 56 | "examples_dirs": "../examples", 57 | # path where to save gallery generated examples 58 | "gallery_dirs": "auto_examples", 59 | "download_all_examples": True, 60 | "backreferences_dir": "backreferences", 61 | "plot_gallery": "True", 62 | } 63 | 64 | # generate autosummary even if no references 65 | autosummary_generate = True 66 | 67 | # The suffix(es) of source filenames. 68 | # You can specify multiple suffix as a list of string: 69 | # source_suffix = ['.rst', '.md'] 70 | source_suffix = ".rst" 71 | 72 | # The encoding of source files. 73 | # source_encoding = 'utf-8-sig' 74 | 75 | # The master toctree document. 76 | master_doc = "index" 77 | 78 | # General information about the project. 79 | project = "nltools" 80 | copyright = f"{time.strftime('%Y')}, Cosan Laboratory" 81 | author = "Cosan Laboratory" 82 | 83 | # The version info for the project you're documenting, acts as replacement for 84 | # |version| and |release|, also used in various other places throughout the 85 | # built documents. 86 | # 87 | # The short X.Y version. 88 | version = version 89 | 90 | # The full version, including alpha/beta/rc tags. 91 | release = version 92 | 93 | # The language for content autogenerated by Sphinx. Refer to documentation 94 | # for a list of supported languages. 95 | # 96 | # This is also used if you do content translation via gettext catalogs. 97 | # Usually you set "language" from the command line for these cases. 98 | language = None 99 | 100 | # There are two options for replacing |today|: either, you set today to some 101 | # non-false value, then it is used: 102 | # today = '' 103 | # Else, today_fmt is used as the format for a strftime call. 104 | # today_fmt = '%B %d, %Y' 105 | 106 | # List of patterns, relative to source directory, that match files and 107 | # directories to ignore when looking for source files. 108 | exclude_patterns = ["_build"] 109 | 110 | # The reST default role (used for this markup: `text`) to use for all 111 | # documents. 112 | # default_role = None 113 | 114 | # If true, '()' will be appended to :func: etc. cross-reference text. 115 | add_function_parentheses = True 116 | 117 | # If true, the current module name will be prepended to all description 118 | # unit titles (such as .. function::). 119 | add_module_names = True 120 | 121 | # If true, sectionauthor and moduleauthor directives will be shown in the 122 | # output. They are ignored by default. 123 | show_authors = True 124 | 125 | # The name of the Pygments (syntax highlighting) style to use. 126 | pygments_style = "sphinx" 127 | 128 | # A list of ignored prefixes for module index sorting. 129 | # modindex_common_prefix = [] 130 | 131 | # If true, keep warnings as "system message" paragraphs in the built documents. 132 | # keep_warnings = False 133 | 134 | # If true, `todo` and `todoList` produce output, else they produce nothing. 135 | todo_include_todos = False 136 | 137 | 138 | # -- Options for HTML output ---------------------------------------------- 139 | # The theme to use for HTML and HTML Help pages. See the documentation for 140 | # a list of builtin themes. 141 | html_theme = "bootstrap" 142 | 143 | # Theme options are theme-specific and customize the look and feel of a theme 144 | # further. For a list of options available for each theme, see the 145 | # documentation. 146 | extlinks = {"github": "https://github.com/cosanlab/nltools"} 147 | 148 | # Add any paths that contain custom themes here, relative to this directory. 149 | html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() 150 | 151 | # The theme to use for HTML and HTML Help pages. See the documentation for 152 | # a list of builtin themes. 153 | # on_rtd is whether we are on readthedocs.org 154 | 155 | # on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 156 | # 157 | # if not on_rtd: # only import and set the theme if we're building docs locally 158 | # import sphinx_rtd_theme 159 | # html_theme = 'sphinx_rtd_theme' 160 | # html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 161 | 162 | # Theme options are theme-specific and customize the look and feel of a theme 163 | # further. For a list of options available for each theme, see the 164 | # documentation. 165 | html_theme_options = { 166 | "bootswatch_theme": "sandstone", 167 | "navbar_sidebarrel": True, 168 | "navbar_pagenav": True, 169 | "bootstrap_version": "3", 170 | "globaltoc_includehidden": "true", 171 | "source_link_position": "footer", 172 | "globaltoc_depth": 2, 173 | "navbar_pagenav_name": "TOC", 174 | "navbar_links": [ 175 | ("Installation", "install"), 176 | ("API", "api"), 177 | ("Tutorials", "auto_examples/index"), 178 | ("Github", "http://www.github.com/ljchang/nltools", True), 179 | ], 180 | } 181 | 182 | # Add any paths that contain custom themes here, relative to this directory. 183 | # html_theme_path = [] 184 | 185 | # The name for this set of Sphinx documents. If None, it defaults to 186 | # " v documentation". 187 | # html_title = None 188 | 189 | # A shorter title for the navigation bar. Default is the same as html_title. 190 | # html_short_title = None 191 | 192 | # The name of an image file (relative to this directory) to place at the top 193 | # of the sidebar. 194 | # html_logo = None 195 | 196 | # The name of an image file (within the static path) to use as favicon of the 197 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 198 | # pixels large. 199 | # html_favicon = None 200 | 201 | # Add any paths that contain custom static files (such as style sheets) here, 202 | # relative to this directory. They are copied after the builtin static files, 203 | # so a file named "default.css" will overwrite the builtin "default.css". 204 | html_static_path = ["_static"] 205 | 206 | # Add any extra paths that contain custom files (such as robots.txt or 207 | # .htaccess) here, relative to this directory. These files are copied 208 | # directly to the root of the documentation. 209 | # html_extra_path = [] 210 | 211 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 212 | # using the given strftime format. 213 | # html_last_updated_fmt = '%b %d, %Y' 214 | 215 | # If true, SmartyPants will be used to convert quotes and dashes to 216 | # typographically correct entities. 217 | # html_use_smartypants = True 218 | 219 | # Custom sidebar templates, maps document names to template names. 220 | # html_sidebars = {} 221 | 222 | # Additional templates that should be rendered to pages, maps page names to 223 | # template names. 224 | # html_additional_pages = {} 225 | 226 | # If false, no module index is generated. 227 | html_domain_indices = True 228 | 229 | # If false, no index is generated. 230 | html_use_index = True 231 | 232 | # If true, the index is split into individual pages for each letter. 233 | # html_split_index = False 234 | 235 | # If true, links to the reST sources are added to the pages. 236 | html_show_sourcelink = True 237 | 238 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 239 | # html_show_sphinx = True 240 | 241 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 242 | html_show_copyright = True 243 | 244 | # If true, an OpenSearch description file will be output, and all pages will 245 | # contain a tag referring to it. The value of this option must be the 246 | # base URL from which the finished HTML is served. 247 | # html_use_opensearch = '' 248 | 249 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 250 | # html_file_suffix = None 251 | 252 | # Language to be used for generating the HTML full-text search index. 253 | # Sphinx supports the following languages: 254 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 255 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 256 | # html_search_language = 'en' 257 | 258 | # A dictionary with options for the search language support, empty by default. 259 | # Now only 'ja' uses this config value 260 | # html_search_options = {'type': 'default'} 261 | 262 | # The name of a javascript file (relative to the configuration directory) that 263 | # implements a search results scorer. If empty, the default will be used. 264 | # html_search_scorer = 'scorer.js' 265 | 266 | # Output file base name for HTML help builder. 267 | htmlhelp_basename = "nltoolsdoc" 268 | 269 | # -- Options for LaTeX output --------------------------------------------- 270 | 271 | # latex_elements = { 272 | # The paper size ('letterpaper' or 'a4paper'). 273 | #'papersize': 'letterpaper', 274 | 275 | # The font size ('10pt', '11pt' or '12pt'). 276 | #'pointsize': '10pt', 277 | 278 | # Additional stuff for the LaTeX preamble. 279 | #'preamble': '', 280 | 281 | # Latex figure (float) alignment 282 | #'figure_align': 'htbp', 283 | # } 284 | 285 | # Grouping the document tree into LaTeX files. List of tuples 286 | # (source start file, target name, title, 287 | # author, documentclass [howto, manual, or own class]). 288 | # latex_documents = [ 289 | # (master_doc, 'nltools.tex', u'nltools Documentation', 290 | # u'Luke Chang', 'manual'), 291 | # ] 292 | 293 | # The name of an image file (relative to this directory) to place at the top of 294 | # the title page. 295 | # latex_logo = None 296 | 297 | # For "manual" documents, if this is true, then toplevel headings are parts, 298 | # not chapters. 299 | # latex_use_parts = False 300 | 301 | # If true, show page references after internal links. 302 | # latex_show_pagerefs = False 303 | 304 | # If true, show URL addresses after external links. 305 | # latex_show_urls = False 306 | 307 | # Documents to append as an appendix to all manuals. 308 | # latex_appendices = [] 309 | 310 | # If false, no module index is generated. 311 | # latex_domain_indices = True 312 | 313 | 314 | # -- Options for manual page output --------------------------------------- 315 | 316 | # One entry per manual page. List of tuples 317 | # (source start file, name, description, authors, manual section). 318 | man_pages = [(master_doc, "nltools", "nltools Documentation", [author], 1)] 319 | 320 | # If true, show URL addresses after external links. 321 | # man_show_urls = False 322 | 323 | 324 | # -- Options for Texinfo output ------------------------------------------- 325 | 326 | # Grouping the document tree into Texinfo files. List of tuples 327 | # (source start file, target name, title, author, 328 | # dir menu entry, description, category) 329 | texinfo_documents = [ 330 | ( 331 | master_doc, 332 | "nltools", 333 | "nltools Documentation", 334 | author, 335 | "nltools", 336 | "One line description of project.", 337 | "Miscellaneous", 338 | ), 339 | ] 340 | 341 | # Documents to append as an appendix to all manuals. 342 | # texinfo_appendices = [] 343 | 344 | # If false, no module index is generated. 345 | # texinfo_domain_indices = True 346 | 347 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 348 | # texinfo_show_urls = 'footnote' 349 | 350 | # If true, do not generate a @detailmenu in the "Top" node's menu. 351 | # texinfo_no_detailmenu = False 352 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | NLTools 2 | ======= 3 | 4 | .. image:: https://img.shields.io/pypi/v/nltools.svg 5 | :target: https://pypi.org/project/nltools/ 6 | 7 | .. image:: https://github.com/cosanlab/nltools/actions/workflows/auto_formatting_tests_and_coverage.yml/badge.svg 8 | :target: https://github.com/cosanlab/nltools/actions/workflows/auto_formatting_tests_and_coverage.yml 9 | 10 | .. image:: https://codecov.io/gh/cosanlab/nltools/branch/master/graph/badge.svg 11 | :target: https://codecov.io/gh/cosanlab/nltools 12 | 13 | .. image:: https://app.codacy.com/project/badge/Grade/f118dc39e5df46d28e80d0d326721cbb 14 | :target: https://app.codacy.com/gh/cosanlab/nltools/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade 15 | 16 | .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.2229813.svg 17 | :target: https://doi.org/10.5281/zenodo.2229813 18 | 19 | .. image:: https://img.shields.io/badge/python-3.7%20%7C%203.8-blue 20 | :target: https://nltools.org 21 | 22 | .. image:: https://img.shields.io/badge/platform-linux%20%7C%20osx%20%7C%20win-blue 23 | :target: https://nltools.org 24 | 25 | `NLTools `_ is a Python package for analyzing neuroimaging data. It is the analysis engine powering `neuro-learn `_ There are tools to perform data manipulation and analyses such as univariate GLMs, predictive multivariate modeling, and representational similarity analyses. It is based loosely off of Tor Wager's `object-oriented Matlab toolbox `_ and leverages much code from `nilearn `_ and `scikit-learn `_ 26 | 27 | Watch a video in which Dr. Eshin Jolly, PhD outlines some of the design principles behind nltools at SciPy 2020. 28 | 29 | .. raw:: html 30 | 31 |
32 | 33 |
34 | 35 | Learn how to use nltools with hands on tutorials: 36 | 37 | - `DartBrains `_ is an introductory neuroimaging analysis course that uses nltools. 38 | - `Naturalistic-Data `_ is a course covering advanced methods for analyzing naturalistic data. Many of the tutorials use functions from nltools. 39 | 40 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | Installation 4 | ------------ 5 | 6 | 1. Method 1 (stable) 7 | 8 | .. code-block:: python 9 | 10 | pip install nltools 11 | 12 | 2. Method 2 (bleeding edge) 13 | 14 | .. code-block:: python 15 | 16 | pip install git+https://github.com/cosanlab/nltools 17 | 18 | 3. Method 3 (for development) 19 | 20 | .. code-block:: python 21 | 22 | git clone https://github.com/cosanlab/nltools 23 | pip install -e nltools 24 | 25 | Preprocessing 26 | ^^^^^^^^^^^^^ 27 | 28 | Nltools has minimal routines for pre-processing data. For more complete pre-processing pipelines please see our `cosanlab_preproc `_ library built with `nipype`. -------------------------------------------------------------------------------- /examples/01_DataOperations/README.txt: -------------------------------------------------------------------------------- 1 | Data Operations 2 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 3 | -------------------------------------------------------------------------------- /examples/01_DataOperations/plot_adjacency.py: -------------------------------------------------------------------------------- 1 | """ 2 | Adjacency Class 3 | =============== 4 | 5 | Nltools has an additional data structure class for working with two-dimensional 6 | square matrices. This can be helpful when working with similarity/distance 7 | matrices or directed or undirected graphs. Similar to the Brain_Data class, 8 | matrices are vectorized and can store multiple matrices in the same object. 9 | This might reflect different brain regions, subjects, or time. Most of the 10 | methods on the Adjacency class are consistent with those in the Brain_Data 11 | class. 12 | 13 | """ 14 | 15 | ######################################################################### 16 | # Load Data 17 | # ---------- 18 | # 19 | # Similar to the Brain_Data class, Adjacency instances can be initialized by passing in a numpy array or pandas data frame, or a path to a csv file or list of files. Here we will generate some fake data to demonstrate how to use this class. In addition to data, you must indicate the type of matrix. Currently, you can specify `['similarity','distance','directed']`. Similarity matrices are symmetrical with typically ones along diagonal, Distance matrices are symmetrical with zeros along diagonal, and Directed graph matrices are not symmetrical. Symmetrical matrices only store the upper triangle. The Adjacency class can also accommodate labels, but does not require them. 20 | 21 | from nltools.data import Adjacency 22 | from scipy.linalg import block_diag 23 | import numpy as np 24 | 25 | m1 = block_diag(np.ones((4, 4)), np.zeros((4, 4)), np.zeros((4, 4))) 26 | m2 = block_diag(np.zeros((4, 4)), np.ones((4, 4)), np.zeros((4, 4))) 27 | m3 = block_diag(np.zeros((4, 4)), np.zeros((4, 4)), np.ones((4, 4))) 28 | noisy = (m1 * 1 + m2 * 2 + m3 * 3) + np.random.randn(12, 12) * 0.1 29 | dat = Adjacency( 30 | noisy, matrix_type="similarity", labels=["C1"] * 4 + ["C2"] * 4 + ["C3"] * 4 31 | ) 32 | 33 | ######################################################################### 34 | # Basic information about the object can be viewed by simply calling it. 35 | 36 | print(dat) 37 | 38 | ######################################################################### 39 | # Adjacency objects can easily be converted back into two-dimensional matrices with the `.squareform()` method. 40 | 41 | dat.squareform() 42 | 43 | ######################################################################### 44 | # Matrices can viewed as a heatmap using the `.plot()` method. 45 | 46 | dat.plot() 47 | 48 | ######################################################################### 49 | # The mean within a a grouping label can be calculated using the `.cluster_summary()` method. You must specify a group variable to group the data. Here we use the labels. 50 | 51 | print(dat.cluster_summary(clusters=dat.labels, summary="within", metric="mean")) 52 | 53 | ######################################################################### 54 | # Regression 55 | # ---------- 56 | # 57 | # Adjacency objects can currently accommodate two different types of regression. Sometimes we might want to decompose an Adjacency matrix from a linear combination of other Adjacency matrices. Other times we might want to perform a regression at each pixel in a stack of Adjacency matrices. Here we provide an example of each method. We use the same data we generated above, but attempt to decompose it by each block of data. We create the design matrix by simply concatenating the matrices we used to create the data object. The regress method returns a dictionary containing all of the relevant information from the regression. Here we show that the model recovers the average weight in each block. 58 | 59 | X = Adjacency([m1, m2, m3], matrix_type="similarity") 60 | stats = dat.regress(X) 61 | print(stats["beta"]) 62 | 63 | ######################################################################### 64 | # In addition to decomposing a single adjacency matrix, we can also estimate a model that predicts the variance over each voxel. This is equivalent to a univariate regression in imaging analyses. Remember that just like in imaging these tests are non-independent and may require correcting for multiple comparisons. Here we create some data that varies over matrices and identify pixels that follow a particular on-off-on pattern. We plot the t-values that exceed 2. 65 | 66 | from nltools.data import Design_Matrix 67 | import matplotlib.pyplot as plt 68 | 69 | data = Adjacency( 70 | [m1 + np.random.randn(12, 12) * 0.5 for x in range(5)] 71 | + [np.zeros((12, 12)) + np.random.randn(12, 12) * 0.5 for x in range(5)] 72 | + [m1 + np.random.randn(12, 12) * 0.5 for x in range(5)] 73 | ) 74 | 75 | X = Design_Matrix([1] * 5 + [0] * 5 + [1] * 5) 76 | f = X.plot() 77 | f.set_title("Model", fontsize=18) 78 | 79 | stats = data.regress(X) 80 | t = stats["t"].plot(vmin=2) 81 | plt.title("Significant Pixels", fontsize=18) 82 | 83 | ######################################################################### 84 | # Similarity/Distance 85 | # ------------------- 86 | # 87 | # We can calculate similarity between two Adjacency matrices using `.similiarity()`. 88 | 89 | stats = dat.similarity(m1) 90 | print(stats) 91 | 92 | ######################################################################### 93 | # We can also calculate the distance between multiple matrices contained within a single Adjacency object. Any distance metric is available from the `sci-kit learn `_ by specifying the `method` flag. This outputs an Adjacency matrix. In the example below we see that several matrices are more similar to each other (i.e., when the signal is on). Remember that the nodes here now represent each matrix from the original distance matrix. 94 | 95 | dist = data.distance(metric="correlation") 96 | dist.plot() 97 | 98 | ######################################################################### 99 | # Similarity matrices can be converted to and from Distance matrices using `.similarity_to_distance()` and `.distance_to_similarity()`. 100 | 101 | dist.distance_to_similarity(metric="correlation").plot() 102 | 103 | ######################################################################### 104 | # Multidimensional Scaling 105 | # ------------------------ 106 | # 107 | # We can perform additional analyses on distance matrices such as multidimensional scaling. Here we provide an example to create a 3D multidimensional scaling plot of our data to see if the on and off matrices might naturally group together. 108 | 109 | dist = data.distance(metric="correlation") 110 | dist.labels = ["On"] * 5 + ["Off"] * 5 + ["On"] * 5 111 | dist.plot_mds(n_components=3) 112 | 113 | ######################################################################### 114 | # Graphs 115 | # ------ 116 | # 117 | # Adjacency matrices can be cast to networkx objects using `.to_graph()` if the optional dependency is installed. This allows any graph theoretic metrics or plots to be easily calculated from Adjacency objects. 118 | 119 | import networkx as nx 120 | 121 | dat = Adjacency(m1 + m2 + m3, matrix_type="similarity") 122 | g = dat.to_graph() 123 | 124 | print("Degree of each node: %s" % g.degree()) 125 | 126 | nx.draw_circular(g) 127 | -------------------------------------------------------------------------------- /examples/01_DataOperations/plot_design_matrix.py: -------------------------------------------------------------------------------- 1 | """ 2 | Design Matrix 3 | ============= 4 | 5 | This tutorial illustrates how to use the Design_Matrix class to flexibly create 6 | design matrices that can then be used with the Brain_Data class to perform 7 | univariate regression. 8 | 9 | Design Matrices can be thought of as "enhanced" pandas dataframes; they can do 10 | everything a pandas dataframe is capable of, with some added features. Design 11 | Matrices follow a data organization format common in many machine learning 12 | applications such as the sci-kit learn API: 2d tables organized as observations 13 | by features. In the context of neuro-imaging this often translates to TRs by 14 | conditions of interest + nuisance covariates (1st level analysis), or 15 | participants by conditions/groups (2nd level analysis). 16 | 17 | """ 18 | 19 | ######################################################################### 20 | # Design Matrix Basics 21 | # -------------------- 22 | # 23 | # Lets just create a basic toy design matrix by hand corresponding to a single participant's data from an experiment with 12 TRs, collected at a temporal resolution of 1.5s. For this example we'll have 4 unique "stimulus conditions" that each occur for 2 TRs (3s) with 1 TR (1.5s) of rest between events. 24 | 25 | from nltools.data import Design_Matrix 26 | import numpy as np 27 | 28 | TR = 1.5 # Design Matrices take a sampling_freq argument specified in hertz which can be converted as 1./TR 29 | 30 | dm = Design_Matrix(np.array([ 31 | [0,0,0,0], 32 | [0,0,0,0], 33 | [1,0,0,0], 34 | [1,0,0,0], 35 | [0,0,0,0], 36 | [0,1,0,0], 37 | [0,1,0,0], 38 | [0,0,0,0], 39 | [0,0,1,0], 40 | [0,0,1,0], 41 | [0,0,0,0], 42 | [0,0,0,1], 43 | [0,0,0,1], 44 | [0,0,0,0], 45 | [0,0,0,0], 46 | [0,0,0,0], 47 | [0,0,0,0], 48 | [0,0,0,0], 49 | [0,0,0,0], 50 | [0,0,0,0], 51 | [0,0,0,0], 52 | [0,0,0,0] 53 | ]), 54 | sampling_freq = 1./TR, 55 | columns=['face_A','face_B','house_A','house_B'] 56 | ) 57 | ######################################################################### 58 | # Notice how this look exactly like a pandas dataframe. That's because design matrices are *subclasses* of dataframes with some extra attributes and methods. 59 | 60 | print(dm) 61 | 62 | ######################################################################### 63 | # Let's take a look at some of that meta-data. We can see that no columns have been convolved as of yet and this design matrix has no polynomial terms (e.g. such as an intercept or linear trend). 64 | 65 | print(dm.details()) 66 | 67 | ######################################################################### 68 | # We can also easily visualize the design matrix using an SPM/AFNI/FSL style heatmap 69 | 70 | dm.heatmap() 71 | 72 | 73 | ######################################################################### 74 | # Adding nuisiance covariates 75 | # --------------------------- 76 | # 77 | # Legendre Polynomials 78 | # ******************** 79 | # 80 | # A common operation is adding an intercept and polynomial trend terms (e.g. linear and quadtratic) as nuisance regressors. This is easy to do. Consistent with other software packages, these are orthogonal Legendre poylnomials on the scale -1 to 1. 81 | 82 | # with include_lower = True (default), 2 here means: 0-intercept, 1-linear-trend, 2-quadtratic-trend 83 | dm_with_nuissance = dm.add_poly(2,include_lower=True) 84 | dm_with_nuissance.heatmap() 85 | 86 | ######################################################################### 87 | # We can see that 3 new columns were added to the design matrix. We can also inspect the change to the meta-data. Notice that the Design Matrix is aware of the existence of three polynomial terms now. 88 | 89 | print(dm_with_nuissance.details()) 90 | 91 | ######################################################################### 92 | # Discrete Cosine Basis Functions 93 | # ******************************* 94 | # 95 | # Polynomial variables are not the only type of nuisance covariates that can be generated for you. Design Matrix also supports the creation of discrete-cosine basis functions ala SPM. This will create a series of filters added as new columns based on a specified duration, defaulting to 180s. Let's create DCT filters for 20s durations in our toy data. 96 | 97 | # Short filter duration for our simple example 98 | dm_with_cosine = dm.add_dct_basis(duration=20) 99 | dm_with_cosine.heatmap() 100 | 101 | ######################################################################### 102 | # Data operations 103 | # --------------- 104 | # 105 | # Performing convolution 106 | # ********************** 107 | # 108 | # Design Matrix makes it easy to perform convolution and will auto-ignore all columns that are consider polynomials. The default convolution kernel is the Glover (1999) HRF parameterized by the glover_hrf implementation in nipy (see nltools.externals.hrf for details). However, any arbitrary kernel can be passed as a 1d numpy array, or multiple kernels can be passed as a 2d numpy array for highly flexible convolution across many types of data (e.g. SCR). 109 | 110 | dm_with_nuissance_c = dm_with_nuissance.convolve() 111 | print(dm_with_nuissance_c.details()) 112 | dm_with_nuissance_c.heatmap() 113 | 114 | ######################################################################### 115 | # Design Matrix can do many different data operations in addition to convolution such as upsampling and downsampling to different frequencies, zscoring, etc. Check out the API documentation for how to use these methods. 116 | 117 | ######################################################################### 118 | # File Reading 119 | # ------------ 120 | # 121 | # Creating a Design Matrix from an onsets file 122 | # ******************************************** 123 | # 124 | # Nltools provides basic file-reading support for 2 or 3 column formatted onset files. Users can look at the onsets_to_dm function as a template to build more complex file readers if desired or to see additional features. Nltools includes an example onsets file where each event lasted exactly 1 TR and TR = 2s. Lets use that to create a design matrix with an intercept and linear trend 125 | 126 | from nltools.utils import get_resource_path 127 | from nltools.file_reader import onsets_to_dm 128 | from nltools.data import Design_Matrix 129 | import os 130 | 131 | TR = 2.0 132 | sampling_freq = 1./TR 133 | onsetsFile = os.path.join(get_resource_path(),'onsets_example.txt') 134 | dm = onsets_to_dm(onsetsFile, sampling_freq=sampling_freq, run_length=160, sort=True,add_poly=1) 135 | dm.heatmap() 136 | 137 | ######################################################################### 138 | # Creating a Design Matrix from a generic csv file 139 | # ************************************************ 140 | # 141 | # Alternatively you can read a generic csv file and transform it into a Design Matrix using pandas file reading capability. Here we'll read in an example covariates file that contains the output of motion realignment estimated during a fMRI preprocessing pipeline. 142 | 143 | import pandas as pd 144 | 145 | covariatesFile = os.path.join(get_resource_path(),'covariates_example.csv') 146 | cov = pd.read_csv(covariatesFile) 147 | cov = Design_Matrix(cov, sampling_freq =sampling_freq) 148 | cov.heatmap(vmin=-1,vmax=1) # alter plot to scale of covs; heatmap takes Seaborn heatmap arguments 149 | 150 | ######################################################################### 151 | # Working with multiple Design Matrices 152 | # ------------------------------------- 153 | # 154 | # Vertically "stacking" Design Matrices 155 | # ************************************* 156 | # A common task is creating a separate design matrix for multiple runs of an experiment, (or multiple subjects) and vertically appending them to each other so that regression can be performed across all runs of an experiment. However, in order to account for run-differences its important (and common practice) to include separate run-wise polynomials (e.g. intercepts). Design Matrix's append method is intelligent and flexible enough to keep columns separated during appending automatically. 157 | 158 | # Lets use the design matrix with polynomials from above 159 | # Stack "run 1" on top of "run 2" 160 | runs_1_and_2 = dm_with_nuissance.append(dm_with_nuissance,axis=0) 161 | runs_1_and_2.heatmap() 162 | 163 | ######################################################################### 164 | # Separating columns during append operations 165 | # ******************************************* 166 | # Notice that all polynomials have been kept separated for you automatically and have been renamed to reflect the fact that they come from different runs. But Design Matrix is even more flexible. Let's say you want to estimate separate run-wise coefficients for all house stimuli too. Simply pass that into the `unique_cols` parameter of append. 167 | 168 | runs_1_and_2 = dm_with_nuissance.append(dm_with_nuissance,unique_cols=['house*'],axis=0) 169 | runs_1_and_2.heatmap() 170 | 171 | ######################################################################### 172 | # Now notice how all stimuli that begin with 'house' have been made into separate columns for each run. In general `unique_cols` can take a list of columns to keep separated or simple wild cards that either begin with a term e.g. `"house*"` or end with one `"*house"`. 173 | 174 | ######################################################################### 175 | # Putting it all together 176 | # ----------------------- 177 | # 178 | # A realistic workflow 179 | # ******************** 180 | # Let's combine all the examples above to build a work flow for a realistic first-level analysis fMRI analysis. This will include loading onsets from multiple experimental runs, and concatenating them into a large multi-run design matrix where we estimate a single set of coefficients for our variables of interest, but make sure we account for run-wise differences nuisiance covarites (e.g. motion) and baseline, trends, etc. For simplicity we'll just reuse the same onsets and covariates file multiple times. 181 | 182 | num_runs = 4 183 | TR = 2.0 184 | sampling_freq = 1./TR 185 | all_runs = Design_Matrix(sampling_freq = sampling_freq) 186 | for i in range(num_runs): 187 | 188 | # 1) Load in onsets for this run 189 | onsetsFile = os.path.join(get_resource_path(),'onsets_example.txt') 190 | dm = onsets_to_dm(onsetsFile, sampling_freq=sampling_freq,run_length=160,sort=True) 191 | 192 | # 2) Convolve them with the hrf 193 | dm = dm.convolve() 194 | 195 | # 2) Load in covariates for this run 196 | covariatesFile = os.path.join(get_resource_path(),'covariates_example.csv') 197 | cov = pd.read_csv(covariatesFile) 198 | cov = Design_Matrix(cov, sampling_freq = sampling_freq) 199 | 200 | # 3) In the covariates, fill any NaNs with 0, add intercept and linear trends and dct basis functions 201 | cov = cov.fillna(0) 202 | 203 | # Retain a list of nuisance covariates (e.g. motion and spikes) which we'll also want to also keep separate for each run 204 | cov_columns = cov.columns 205 | cov = cov.add_poly(1).add_dct_basis() 206 | 207 | # 4) Join the onsets and covariates together 208 | full = dm.append(cov,axis=1) 209 | 210 | # 5) Append it to the master Design Matrix keeping things separated by run 211 | all_runs = all_runs.append(full,axis=0,unique_cols=cov.columns) 212 | 213 | all_runs.heatmap(vmin=-1,vmax=1) 214 | 215 | ######################################################################### 216 | # We can see the left most columns of our multi-run design matrix contain our conditions of interest (stacked across all runs), the middle columns includes separate run-wise nuisiance covariates (motion, spikes) and the right most columns contain run specific polynomials (intercept, trends, etc). 217 | 218 | ######################################################################### 219 | # Data Diagnostics 220 | # ---------------- 221 | # 222 | # Let's actually check if our design is estimable. Design Matrix provides a few tools for cleaning up highly correlated columns (resulting in failure if trying to perform regression), replacing data, and computing collinearity. By default the `clean` method will drop any columns correlated at r >= .95 223 | 224 | all_runs_cleaned = all_runs.clean(verbose=True) 225 | all_runs_cleaned.heatmap(vmin=-1,vmax=1) 226 | 227 | ######################################################################### 228 | # Whoops, looks like above some of our polynomials and dct basis functions are highly correlated, but the clean method detected that and dropped them for us. In practice you'll often include polynomials or dct basis functions rather than both, but this was just an illustrative example. 229 | 230 | ######################################################################### 231 | # Estimating a first-level model 232 | # ------------------------------ 233 | # 234 | # You can now set this multi-run Design Matrix as the `X` attribute of a Brain_Data object containing EPI data for these four runs and estimate a regression in just a few lines of code. 235 | 236 | # This code is commented because we don't actually have niftis loaded for the purposes of this tutorial 237 | # See the other tutorials for more details on working with nifti files and Brain_Data objects 238 | 239 | # Assuming you already loaded up Nifti images like this 240 | # list_of_niftis = ['run_1.nii.gz','run_2.nii.gz','run_3.nii.gz','run_4.nii.gz'] 241 | # all_run_data = Brain_Data(list_of_niftis) 242 | 243 | # Set our Design Matrix to the X attribute of Brain_Data object 244 | # all_run_data.X = all_runs_cleaned 245 | 246 | # Run the regression 247 | # results = all_run_data.regress() 248 | 249 | # This will produce N beta, t, and p images 250 | # where N is the number of columns in the design matrix 251 | -------------------------------------------------------------------------------- /examples/01_DataOperations/plot_download.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic Data Operations 3 | ===================== 4 | 5 | A simple example showing how to download a dataset from neurovault and perform 6 | basic data operations. The bulk of the nltools toolbox is built around the 7 | Brain_Data() class. This class represents imaging data as a vectorized 8 | features by observations matrix. Each image is an observation and each voxel 9 | is a feature. The concept behind the class is to have a similar feel to a pandas 10 | dataframe, which means that it should feel intuitive to manipulate the data. 11 | 12 | """ 13 | 14 | ######################################################################### 15 | # Download pain dataset from neurovault 16 | # --------------------------------------------------- 17 | # 18 | # Here we fetch the pain dataset used in `Chang et al., 2015 `_ 19 | # from `neurovault `_. In this dataset 20 | # there are 28 subjects with 3 separate beta images reflecting varying intensities 21 | # of thermal pain (i.e., high, medium, low). The data will be downloaded to ~/nilearn_data, 22 | # and automatically loaded as a Brain_Data() instance. The image metadata will be stored in data.X. 23 | 24 | from nltools.datasets import fetch_pain 25 | 26 | data = fetch_pain() 27 | 28 | ######################################################################### 29 | # Load files 30 | # --------------------------------------------------- 31 | # 32 | # Nifti images can be easily loaded simply by passing a string to a nifti file. 33 | # Many images can be loaded together by passing a list of nifti files. 34 | # For example, on linux or OSX systmes, the downloads from fetch_pain() will be 35 | # stored in ~/nilearn_data. We will load subject 1's data. 36 | 37 | # NOTES: Need to figure out how to get path to data working on rtd server 38 | # from nltools.data import Brain_Data 39 | # import glob 40 | # 41 | # sub1 = Brain_Data(glob.glob('~/nilearn_data/chang2015_pain/Pain_Subject_1*.nii.gz')) 42 | 43 | ######################################################################### 44 | # Basic Brain_Data() Operations 45 | # --------------------------------------------------------- 46 | # 47 | # Here are a few quick basic data operations. 48 | # Find number of images in Brain_Data() instance 49 | 50 | print(len(data)) 51 | 52 | ######################################################################### 53 | # Find the dimensions of the data. images x voxels 54 | 55 | print(data.shape()) 56 | 57 | ######################################################################### 58 | # We can use any type of indexing to slice the data such as integers, lists 59 | # of integers, or boolean. 60 | 61 | print(data[[1,6,2]]) 62 | 63 | ######################################################################### 64 | # Calculate the mean for every voxel over images 65 | 66 | data.mean() 67 | 68 | ######################################################################### 69 | # Calculate the standard deviation for every voxel over images 70 | 71 | data.std() 72 | 73 | ######################################################################### 74 | # Methods can be chained. Here we get the shape of the mean. 75 | 76 | print(data.mean().shape()) 77 | 78 | ######################################################################### 79 | # Brain_Data instances can be added and subtracted 80 | 81 | new = data[1]+data[2] 82 | 83 | ######################################################################### 84 | # Brain_Data instances can be manipulated with basic arithmetic operations 85 | # Here we add 10 to every voxel and scale by 2 86 | 87 | data2 = (data+10)*2 88 | 89 | ######################################################################### 90 | # Brain_Data instances can be copied 91 | 92 | new = data.copy() 93 | 94 | ######################################################################### 95 | # Brain_Data instances can be easily converted to nibabel instances, which 96 | # store the data in a 3D/4D matrix. This is useful for interfacing with other 97 | # python toolboxes such as `nilearn `_ 98 | 99 | data.to_nifti() 100 | 101 | ######################################################################### 102 | # Brain_Data instances can be concatenated using the append method 103 | 104 | new = new.append(data[4]) 105 | 106 | ######################################################################### 107 | # Any Brain_Data object can be written out to a nifti file 108 | 109 | data.write('Tmp_Data.nii.gz') 110 | 111 | ######################################################################### 112 | # Images within a Brain_Data() instance are iterable. Here we use a list 113 | # comprehension to calculate the overall mean across all voxels within an 114 | # image. 115 | 116 | [x.mean() for x in data] 117 | 118 | ######################################################################### 119 | # Basic Brain_Data() Plotting 120 | # --------------------------------------------------------- 121 | # 122 | # There are multiple ways to plot data. First, Brain_Data() instances can be 123 | # converted to a nibabel instance and plotted using any plot method such as 124 | # nilearn. 125 | 126 | from nilearn.plotting import plot_glass_brain 127 | 128 | plot_glass_brain(data.mean().to_nifti()) 129 | 130 | ######################################################################### 131 | # There is also a fast montage plotting method. Here we plot the average image 132 | # it will render a separate plot for each image. There is a 'limit' flag 133 | # which allows you to specify the maximum number of images to display. 134 | 135 | data.mean().plot() 136 | -------------------------------------------------------------------------------- /examples/01_DataOperations/plot_mask.py: -------------------------------------------------------------------------------- 1 | """ 2 | Masking Example 3 | =============== 4 | 5 | This tutorial illustrates methods to help with masking data. 6 | 7 | """ 8 | 9 | ######################################################################### 10 | # Load Data 11 | # --------- 12 | # 13 | # First, let's load the pain data for this example. 14 | 15 | from nltools.datasets import fetch_pain 16 | 17 | data = fetch_pain() 18 | 19 | ######################################################################### 20 | # Apply_Mask 21 | # ---------- 22 | # 23 | # Spherical masks can be created using the create_sphere function. 24 | # It requires specifying a center voxel and the radius of the sphere. 25 | 26 | from nltools.mask import create_sphere 27 | 28 | mask = create_sphere([0, 0, 0], radius=30) 29 | masked_data = data.apply_mask(mask) 30 | masked_data.mean().plot() 31 | 32 | ######################################################################### 33 | # Extract Mean Within ROI 34 | # ----------------------- 35 | # 36 | # We can easily calculate the mean within an ROI for each image within a 37 | # Brain_Data() instance using the extract_roi() method. 38 | 39 | import matplotlib.pyplot as plt 40 | 41 | mean = data.extract_roi(mask) 42 | plt.plot(mean) 43 | 44 | ######################################################################### 45 | # Expand and Contract ROIs 46 | # ------------------------ 47 | # 48 | # Some masks have many ROIs indicated by a unique ID. It is possible to 49 | # expand these masks into separate ROIs and also collapse them into a single 50 | # image again. Here we will demonstrate on a k=50 parcellation hosted on 51 | # http://neurovault.org. 52 | 53 | from nltools.mask import expand_mask, collapse_mask 54 | from nltools.data import Brain_Data 55 | 56 | mask = Brain_Data('http://neurovault.org/media/images/2099/Neurosynth%20Parcellation_0.nii.gz') 57 | mask.plot() 58 | 59 | ######################################################################### 60 | # We can expand this mask into 50 separate regions 61 | 62 | mask_x = expand_mask(mask) 63 | mask_x[:3].plot() 64 | 65 | ######################################################################### 66 | # We can collapse these 50 separate regions as unique values in a single image 67 | 68 | mask_c = collapse_mask(mask_x) 69 | mask_c.plot() 70 | 71 | ######################################################################### 72 | # Threshold and Regions 73 | # --------------------- 74 | # 75 | # Images can be thresholded using an arbitrary cutoff or a percentile using the 76 | # threshold method. Here we calculate the mean of the high pain images and 77 | # threshold using the 95 percentile. 78 | 79 | high = data[data.X['PainLevel']==3] 80 | high.mean().threshold(lower='2.5%', upper='97.5%').plot() 81 | 82 | ######################################################################### 83 | # We might be interested in creating a binary mask from this threshold. 84 | 85 | mask_b = high.mean().threshold(lower='2.5%', upper='97.5%',binarize=True) 86 | mask_b.plot() 87 | 88 | ######################################################################### 89 | # We might also want to create separate images from each contiguous ROI. 90 | 91 | region = high.mean().threshold(lower='2.5%', upper='97.5%').regions() 92 | region.plot() 93 | 94 | ######################################################################### 95 | # Finally, we can perform operations on ROIs from a mask and then convert them 96 | # back into a Brain_Data instance. In this example, let's compute a linear contrast 97 | # of increasing pain for each each participant. Then, let's compute functional 98 | # connectivity across participants within each ROI and calculate the degree 99 | # centrality of each ROI after arbitrarily thresholding the connectivity matrix. 100 | # We can then convert each ROIs degree back into a Brain_Data instance to help 101 | # visualize which regions are more central in this analysis. 102 | 103 | from sklearn.metrics import pairwise_distances 104 | from nltools.data import Adjacency 105 | from nltools.mask import roi_to_brain 106 | import pandas as pd 107 | import numpy as np 108 | 109 | sub_list = data.X['SubjectID'].unique() 110 | 111 | # perform matrix multiplication to compute linear contrast for each subject 112 | lin_contrast = [] 113 | for sub in sub_list: 114 | lin_contrast.append(data[data.X['SubjectID'] == sub] * np.array([1, -1, 0])) 115 | 116 | # concatenate list of Brain_Data instances into a single instance 117 | lin_contrast = Brain_Data(lin_contrast) 118 | 119 | # Compute correlation distance between each ROI 120 | dist = Adjacency(pairwise_distances(lin_contrast.extract_roi(mask), metric='correlation'), matrix_type='distance') 121 | 122 | # Threshold functional connectivity and convert to Adjacency Matrix. Plot as heatmap 123 | dist.threshold(upper=.4, binarize=True).plot() 124 | 125 | # Convert Adjacency matrix to networkX instance 126 | g = dist.threshold(upper=.4, binarize=True).to_graph() 127 | 128 | # Compute degree centrality and convert back into Brain_Data instance. 129 | degree_centrality = roi_to_brain(pd.Series(dict(g.degree())), mask_x) 130 | 131 | degree_centrality.plot() -------------------------------------------------------------------------------- /examples/01_DataOperations/plot_mni_prefs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Brain resolution and MNI Template Preferences 3 | ============================================= 4 | 5 | By default nltools uses a 2mm MNI template which means all `Brain_Data` operations will automatically be resampled to that space if they aren't already at that resolution. If you know you want to work in another space you can set that for all operations using the prefs module: 6 | """ 7 | 8 | ######################################################################### 9 | # Setting GLOBAL MNI template preferences 10 | # --------------------- 11 | # 12 | from nltools.prefs import MNI_Template, resolve_mni_path 13 | from nltools.data import Brain_Data 14 | from nltools.simulator import Simulator # just for dummy data 15 | 16 | ######################################################################### 17 | # Here we create some dummy data. Notice that it defaults to 2mm resolution. You can verify this by seeing that the voxel count is approximately 240k: 18 | dummy_brain = Simulator().create_data([0, 1], 1, reps=3) 19 | dummy_brain.write("dummy_2mm_brain.nii.gz") # save it for later 20 | dummy_brain # default 2mm resolution 21 | 22 | ######################################################################### 23 | # You can also get the exact file locations of the currently loaded default template and masks: 24 | resolve_mni_path(MNI_Template) 25 | 26 | ######################################################################### 27 | # To update this simply change the resolution attribute of the MNI_Template. NOTE: that this will change **all** subsequent Brain_Data operations to utilize this new space. Therefore we **highly recommend** doing this at the top of any analysis notebook or script you use to prevent unexpected results 28 | MNI_Template.resolution = 3 # passing the string '3mm' also works 29 | dummy_brain_3mm = Simulator().create_data([0, 1], 1, reps=3) 30 | dummy_brain_3mm # should be 3mm 31 | 32 | ######################################################################### 33 | # The voxel count is now ~70k and you can see the file paths of the global template: 34 | 35 | resolve_mni_path(MNI_Template) 36 | 37 | ######################################################################### 38 | # Notice that when we load we load the previous 2mm brain, it's **automatically** resampled to the currently set default MNI template (3mm): 39 | loaded_brain = Brain_Data("dummy_2mm_brain.nii.gz") 40 | loaded_brain # now in 3mm space! 41 | 42 | ######################################################################### 43 | # Setting local resolution preferences 44 | # ------------------------------------ 45 | # 46 | # If you want to override the global setting on a case-by-case basis, simply use the `mask` argument in `Brain_Data`. This will resample data to the resolution of the `mask` ignoring whatever `MNI_Template` is set to: 47 | 48 | # Here we save the 3mm path as a variable, but in your own data you can provide 49 | # the location of any nifti file 50 | mask_file_3mm = resolve_mni_path(MNI_Template)["mask"] 51 | 52 | MNI_Template.resolution = 2 # reset the global MNI template to 2mm 53 | load_using_default = Brain_Data("dummy_2mm_brain.nii.gz") 54 | load_using_default # 2mm space 55 | 56 | ######################################################################### 57 | load_using_3mm_mask = Brain_Data("dummy_2mm_brain.nii.gz", mask=mask_file_3mm) 58 | load_using_3mm_mask # resampled to 3mm space because a mask was provided 59 | 60 | ######################################################################### 61 | # Notice that the global setting is still 2mm, but by providing a `mask` we were able to override it 62 | 63 | resolve_mni_path(MNI_Template) 64 | -------------------------------------------------------------------------------- /examples/01_DataOperations/plot_neurovault_io.py: -------------------------------------------------------------------------------- 1 | """ 2 | Neurovault I/O 3 | ============== 4 | 5 | Data can be easily downloaded and uploaded to `neurovault `_ 6 | using `pynv `_, a python wrapper for the 7 | neurovault api. 8 | 9 | """ 10 | 11 | ######################################################################### 12 | # Download a Collection 13 | # --------------------- 14 | # 15 | # Entire collections from neurovault can be downloaded along with the 16 | # accompanying image metadata. You just need to know the collection ID. 17 | # Data will be downloaded to the path specified in the 'data_dir' flag 18 | # or '~/nilearn_data' by default. These files can then be imported into 19 | # nltools as a Brain_Data() instance. 20 | 21 | from nltools.datasets import download_collection 22 | from nltools.data import Brain_Data 23 | 24 | metadata,files = download_collection(collection=2099) 25 | mask = Brain_Data(files,X=metadata) 26 | 27 | ######################################################################### 28 | # Download a Single Image from the Web 29 | # ------------------------------------ 30 | # 31 | # It's possible to load a single image from a web URL using the Brain_Data 32 | # load method. The files are downloaded to a temporary directory and will 33 | # eventually be erased by your computer so be sure to write it out to a file 34 | # if you would like to save it. Here we plot it using nilearn's glass brain 35 | # function. 36 | 37 | from nilearn.plotting import plot_glass_brain 38 | 39 | mask = Brain_Data('http://neurovault.org/media/images/2099/Neurosynth%20Parcellation_0.nii.gz') 40 | 41 | plot_glass_brain(mask.to_nifti()) 42 | 43 | ######################################################################### 44 | # Upload Data to Neurovault 45 | # ------------------------- 46 | # 47 | # There is a method to easily upload a Brain_Data() instance to 48 | # `neurovault `_. This requires using your api key, which can be found 49 | # under your account settings. Anything stored in data.X will be uploaded as 50 | # image metadata. The required fields include collection_name, the img_type, 51 | # img_modality, and analysis_level. See https://github.com/neurolearn/pyneurovault_upload 52 | # for additional information about the required fields. (Don't forget to uncomment the line!) 53 | 54 | api_key = 'your_neurovault_api_key' 55 | 56 | # mask.upload_neurovault(access_token=api_key, collection_name='Neurosynth Parcellation', 57 | # img_type='Pa', img_modality='Other',analysis_level='M') 58 | 59 | -------------------------------------------------------------------------------- /examples/02_Analysis/README.txt: -------------------------------------------------------------------------------- 1 | Analysis Examples 2 | ^^^^^^^^^^^^^^^^^ 3 | -------------------------------------------------------------------------------- /examples/02_Analysis/plot_decomposition.py: -------------------------------------------------------------------------------- 1 | """ 2 | Decomposition 3 | ============= 4 | 5 | Here we demonstrate how to perform a decomposition of an imaging dataset. 6 | All you need to do is specify the algorithm. Currently, we have several 7 | different algorithms implemented from 8 | `scikit-learn `_ 9 | ('PCA','ICA','Factor Analysis','Non-Negative Matrix Factorization'). 10 | 11 | """ 12 | 13 | ######################################################################### 14 | # Load Data 15 | # --------- 16 | # 17 | # First, let's load the pain data for this example. We need to specify the 18 | # training levels. We will grab the pain intensity variable from the data.X 19 | # field. 20 | 21 | from nltools.datasets import fetch_pain 22 | 23 | data = fetch_pain() 24 | 25 | ######################################################################### 26 | # Center within subject 27 | # --------------------- 28 | # 29 | # Next we will center the data. However, because we are combining three pain 30 | # image intensities, we will perform centering separately for each participant. 31 | 32 | data_center = data.empty() 33 | for s in data.X['SubjectID'].unique(): 34 | sdat = data[data.X['SubjectID']==s] 35 | data_center = data_center.append(sdat.standardize()) 36 | 37 | 38 | ######################################################################### 39 | # Decomposition with Factor Analysis 40 | # ---------------------------------- 41 | # 42 | # We can now decompose the data into a subset of factors. For this example, 43 | # we will use factor analysis, but we can easily switch out the algorithm with 44 | # either 'pca', 'ica', or 'nnmf'. Decomposition can be performed over voxels 45 | # or alternatively over images. Here we perform decomposition over images, 46 | # which means that voxels are the observations and images are the features. Set 47 | # axis='voxels' to decompose voxels treating images as observations. 48 | 49 | n_components = 5 50 | 51 | output = data_center.decompose(algorithm='fa', axis='images', 52 | n_components=n_components) 53 | 54 | 55 | ######################################################################### 56 | # Display the available data in the output dictionary. The output contains 57 | # a Brain_Data instance with the brain factors (e.g., output['components']), 58 | # the feature by component weighting matrix (output['weights']), and the 59 | # scikit-learn decomposition object (output['decomposition_object']. 60 | # The Decomposition object contains the full set of information, including 61 | # the parameters, the components, and the explained variance. 62 | 63 | print(output.keys()) 64 | 65 | ######################################################################### 66 | # Next, we can plot the results. Here we plot a heatmap of how each 67 | # brain image loads on each component. We also plot the degree to which 68 | # each voxel loads on each component. 69 | 70 | import seaborn as sns 71 | import matplotlib.pylab as plt 72 | 73 | with sns.plotting_context(context='paper', font_scale=2): 74 | sns.heatmap(output['weights']) 75 | plt.ylabel('Images') 76 | plt.xlabel('Components') 77 | 78 | output['components'].plot(limit=n_components) 79 | 80 | ######################################################################### 81 | # Finally, we can examine if any of the components track the intensity of 82 | # pain. We plot the average loading of each component onto each pain 83 | # intensity level. Interestingly, the first component with positive weights 84 | # on the bilateral insula, s2, and ACC monotonically tracks the pain 85 | # intensity level. 86 | 87 | import pandas as pd 88 | 89 | wt = pd.DataFrame(output['weights']) 90 | wt['PainIntensity'] = data_center.X['PainLevel'].replace({1:'Low', 91 | 2:'Medium', 92 | 3:'High'} 93 | ).reset_index(drop=True) 94 | 95 | wt_long = pd.melt(wt, 96 | value_vars=range(n_components), 97 | value_name='Weight', 98 | var_name='Component', 99 | id_vars='PainIntensity') 100 | 101 | with sns.plotting_context(context='paper', font_scale=2): 102 | sns.catplot(data=wt_long, 103 | y='Weight', 104 | x='PainIntensity', 105 | hue='Component', 106 | order=['Low','Medium','High'], 107 | aspect=1.5) 108 | -------------------------------------------------------------------------------- /examples/02_Analysis/plot_hyperalignment.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functional Alignment 3 | ==================== 4 | 5 | When performing any type of group analysis, we assume that each voxel is 6 | reflecting the same computations across all participants. This assumption is 7 | unlikely to be true. Several standard preprocessing steps assist in improving 8 | 'anatomical alignment'. We spatially normalize to a common anatomical template 9 | and we also apply spatial smoothing to improve signal to noise ratios in a 10 | target voxel by averaging activity in surrounding voxels with a gaussian kernel. 11 | However, these techniques are limited when learning multivariate models, where 12 | voxel alignment across participants is critical to making accurate inference. 13 | There have been several developments in improving 'functional alignment'. 14 | Jim Haxby's group has pioneered `hyperalignment 15 | `_, which uses an 16 | iterative procrustes transform to scale, rotate, and reflect voxel time series 17 | so that they are in the same functional space across participants. They have 18 | found that this technique can dramatically improve between subject 19 | classification accuracy particularly in `ventral temporal cortex 20 | `_. This technique is 21 | implemented in the `PyMVPA `_ toolbox. Another promising 22 | functional alignment technique known as the `Shared Response Model `_ 23 | was developed at Princeton to improve intersubject-connectivity analyses and is 24 | implemented in the `brainiak `_ toolbox. 25 | They also have found that this technique can improve between subject analyses. 26 | This method has several additional interesting properties such as the ability 27 | to learn a lower dimensional common representational space and also a 28 | probabilistic implementation. In this tutorial we demonstrate how to perform 29 | functional alignment using both hyperalignment and the shared response model 30 | using nltools. 31 | 32 | """ 33 | 34 | ######################################################################### 35 | # Simulate Data 36 | # ------------- 37 | # 38 | # First, let's simulate some data to align. Here we will simulate 3 subjects 39 | # with 100 data points. Each subject has signal in 30% of the voxels in the 40 | # MPFC with noise. 41 | 42 | import numpy as np 43 | from nltools.mask import create_sphere 44 | from nltools.data import Brain_Data 45 | import matplotlib.pyplot as plt 46 | from nilearn.plotting import plot_glass_brain 47 | 48 | n_observations = 500 49 | p = .3 50 | sigma = 1 51 | n_sub = 3 52 | 53 | y = np.zeros(n_observations) 54 | y[np.arange(75,150)] = 4 55 | y[np.arange(200,250)] = 10 56 | y[np.arange(300,475)] = 7 57 | 58 | def simulate_data(n_observations, y, p, sigma, mask): 59 | ''' Simulate Brain Data 60 | 61 | Args: 62 | n_observations: (int) number of data points 63 | y: (array) one dimensional array of signal 64 | p: (float) probability of signal in voxels 65 | sigma: (float) amount of gaussian noise to add 66 | 67 | Returns: 68 | data: (list) of Brain_Data objects 69 | ''' 70 | 71 | dat = Brain_Data(mask).apply_mask(mask) 72 | new_data = np.zeros((dat.shape()[0], n_observations)) 73 | for i in np.where(dat.data==1)[0]: 74 | if np.random.randint(0,high=10) < p: 75 | new_data[i,:] = y 76 | noise = np.random.randn(new_data.shape[0],n_observations)*sigma 77 | dat.data = (new_data+noise).T 78 | return dat 79 | 80 | mask = create_sphere([0, 45, 0], radius=8) 81 | data = [simulate_data(n_observations, y, p, sigma, mask) for x in range(n_sub)] 82 | 83 | plt.figure(figsize=(10,3)) 84 | plt.plot(y) 85 | plt.title('Simulated Signal', fontsize=20) 86 | plt.xlabel('Time', fontsize=18) 87 | plt.ylabel('Signal', fontsize=18) 88 | plot_glass_brain(data[0].mean().to_nifti()) 89 | 90 | ######################################################################### 91 | # Hyperalign Data 92 | # --------------- 93 | # 94 | # We will now align voxels with the same signal across participants. We will 95 | # start using hyperalignment with the procrustes transform. The align function 96 | # takes a list of Brain_Data objects (or numpy matrices) and aligns voxels based 97 | # on similar responses over time. The function outputs a dictionary with keys 98 | # for a list of the transformed data, corresponding transofmration matrices and 99 | # scaling terms. In addition it returns the "common model" in which all 100 | # subjects are projected. The disparity values correspond to the multivariate 101 | # distance of the subject to the common space. 102 | 103 | from nltools.stats import align 104 | 105 | out = align(data, method='procrustes') 106 | 107 | print(out.keys()) 108 | 109 | 110 | ######################################################################### 111 | # Plot Transformed Data 112 | # --------------------- 113 | # 114 | # To make it more clear what it is happening we plot the voxel by time matrices 115 | # separately for each subject. It is clear that there is a consistent signal 116 | # across voxels, but that the signal is distributed across 'different' voxels. 117 | # The transformed data shows the voxels for each subject aligned to the common 118 | # space. This now permits inferences across the voxels. As an example, we 119 | # plot the matrices of the original compared to the aligned data across subjects. 120 | 121 | f,a = plt.subplots(nrows=2, ncols=3, figsize=(15,5), sharex=True, sharey=True) 122 | [a[0,i].imshow(x.data.T, aspect='auto') for i,x in enumerate(data)] 123 | [a[1,i].imshow(x.data.T, aspect='auto') for i,x in enumerate(out['transformed'])] 124 | a[0,0].set_ylabel('Original Voxels', fontsize=16) 125 | a[1,0].set_ylabel('Aligned Features', fontsize=16) 126 | [a[1,x].set_xlabel('Time', fontsize=16) for x in range(3)] 127 | [a[0,x].set_title('Subject %s' % str(x+1), fontsize=16) for x in range(3)] 128 | plt.tight_layout() 129 | 130 | f,a = plt.subplots(ncols=2, figsize=(15,5), sharex=True, sharey=True) 131 | a[0].imshow(np.mean(np.array([x.data.T for x in data]), axis=0), aspect='auto') 132 | a[1].imshow(np.mean(np.array([x.data.T for x in out['transformed']]), axis=0), aspect='auto') 133 | a[0].set_ylabel('Voxels', fontsize=16) 134 | [a[x].set_xlabel('Time', fontsize=16) for x in range(2)] 135 | a[0].set_title('Average Voxel x Time Matrix of Original Data', fontsize=16) 136 | a[1].set_title('Average Voxel x Time Matrix of Aligned Data', fontsize=16) 137 | 138 | 139 | ######################################################################### 140 | # Transform aligned data back into original subject space 141 | # ------------------------------------------------------- 142 | # 143 | # The transformation matrices can be used to project each subject's aligned 144 | # data into the original subject specific voxel space. The procrustes method 145 | # doesn't look identical as there are a few processing steps that occur within 146 | # the algorithm that would need to be accounted for to fully recover the original 147 | # data (e.g., centering, and scaling by norm). 148 | 149 | backprojected = [np.dot(t.data, tm.data) for t,tm, in zip(out['transformed'], out['transformation_matrix'])] 150 | 151 | f,a = plt.subplots(nrows=3, ncols=3, figsize=(15,10), sharex=True, sharey=True) 152 | [a[0, i].imshow(x.data.T, aspect='auto') for i, x in enumerate(data)] 153 | [a[1, i].imshow(x.data.T, aspect='auto') for i, x in enumerate(out['transformed'])] 154 | [a[2, i].imshow(x.T, aspect='auto') for i, x in enumerate(backprojected)] 155 | [a[i, 0].set_ylabel(x,fontsize=16) for i, x in enumerate(['Original Voxels','Aligned Features', 'Backprojected Voxels'])] 156 | [a[2, x].set_xlabel('Time', fontsize=16) for x in range(3)] 157 | [a[0, x].set_title('Subject %s' % str(x+1), fontsize=16) for x in range(3)] 158 | plt.tight_layout() 159 | 160 | ######################################################################### 161 | # Align new subject to common model 162 | # --------------------------------- 163 | # 164 | # We can also align a new subject to the common model without retraining the 165 | # entire model. Here we individually align subject 3 to the common space 166 | # learned above. We also backproject the transformed subject's data into the 167 | # original subject voxel space. 168 | 169 | d3 = data[2] 170 | d3_out = d3.align(out['common_model'], method='procrustes') 171 | bp = np.dot(d3_out['transformed'].data, d3_out['transformation_matrix'].data) 172 | 173 | f,a = plt.subplots(ncols=3, figsize=(15,5), sharex=True, sharey=True) 174 | a[0].imshow(d3.data.T, aspect='auto') 175 | a[1].imshow(d3_out['transformed'].data.T, aspect='auto') 176 | a[2].imshow(bp.T, aspect='auto') 177 | [a[i].set_title(x,fontsize=18) for i, x in enumerate(['Original Data',' Transformed_Data', 'Backprojected Data'])] 178 | [a[x].set_xlabel('Time', fontsize=16) for x in range(2)] 179 | a[0].set_ylabel('Voxels', fontsize=16) 180 | plt.tight_layout() 181 | 182 | ######################################################################### 183 | # Align subjects in lower dimensional common space 184 | # ------------------------------------------------ 185 | # 186 | # The shared response model allows for the possibility of aligning in a lower 187 | # dimensional functional space. Here we provide an example of aligning to a 10 188 | # dimensional features space. Previous work has found that this can potentially 189 | # improve generalizability of multivariate models trained on an ROI compared to 190 | # using as many features as voxels. This feature is not yet implemented for 191 | # procrustes transformation as dimensionality reduction would need to happen 192 | # either before or after alignment. 193 | 194 | n_features = 10 195 | out = align(data, method='probabilistic_srm', n_features=n_features) 196 | 197 | backprojected = [np.dot(t, tm.data) for t,tm in zip(out['transformed'],out['transformation_matrix'])] 198 | 199 | f,a = plt.subplots(nrows=3, ncols=3, figsize=(15,10), sharex=True, sharey=False) 200 | [a[0, i].imshow(x.data.T, aspect='auto') for i, x in enumerate(data)] 201 | [a[1, i].imshow(x.T, aspect='auto') for i, x in enumerate(out['transformed'])] 202 | [a[2, i].imshow(x.T, aspect='auto') for i, x in enumerate(backprojected)] 203 | [a[i, 0].set_ylabel(x, fontsize=16) for i, x in enumerate(['Original Voxels','Aligned Features', 'Backprojected Voxels'])] 204 | [a[2, x].set_xlabel('Time', fontsize=16) for x in range(3)] 205 | [a[0, x].set_title('Subject %s' % str(x+1), fontsize=16) for x in range(3)] 206 | plt.tight_layout() 207 | 208 | -------------------------------------------------------------------------------- /examples/02_Analysis/plot_multivariate_classification.py: -------------------------------------------------------------------------------- 1 | """ 2 | Multivariate Classification 3 | =========================== 4 | 5 | This tutorial provides an example of how to run classification analyses. 6 | 7 | """ 8 | 9 | ######################################################################### 10 | # Load & Prepare Data 11 | # ------------------- 12 | # 13 | # First, let's load the pain data for this example. We need to create a data 14 | # object with high and low pain intensities. These labels need to be specified in the 15 | # dat.Y field as a pandas dataframe. We also need to create a vector of subject ids 16 | # so that subject images can be held out together in cross-validation. 17 | 18 | from nltools.datasets import fetch_pain 19 | import numpy as np 20 | import pandas as pd 21 | 22 | data = fetch_pain() 23 | high = data[np.where(data.X['PainLevel']==3)[0]] 24 | low = data[np.where(data.X['PainLevel']==1)[0]] 25 | dat = high.append(low) 26 | dat.Y = pd.DataFrame(np.concatenate([np.ones(len(high)),np.zeros(len(low))])) 27 | subject_id = np.concatenate([high.X['SubjectID'].values,low.X['SubjectID'].values]) 28 | 29 | ######################################################################### 30 | # Classification with Cross-Validation 31 | # ------------------------------------ 32 | # 33 | # We can now train a brain model to classify the different labels specified in dat.Y. 34 | # First, we will use a support vector machine with 5 fold cross-validation in which the 35 | # same images from each subject are held out together. 36 | # The predict function runs the classification multiple times. One of the 37 | # iterations uses all of the data to calculate the 'weight_map'. The other iterations 38 | # estimate the cross-validated predictive accuracy. 39 | 40 | svm_stats = dat.predict(algorithm='svm', 41 | cv_dict={'type': 'kfolds','n_folds': 5, 'subject_id':subject_id}, 42 | **{'kernel':"linear"}) 43 | 44 | ######################################################################### 45 | # SVMs can be converted to predicted probabilities using Platt Scaling 46 | 47 | platt_stats = dat.predict(algorithm='svm', 48 | cv_dict={'type': 'kfolds','n_folds': 5, 'subject_id':subject_id}, 49 | **{'kernel':'linear','probability':True}) 50 | 51 | ######################################################################### 52 | # Standard OLS Logistic Regression. 53 | logistic_stats = dat.predict(algorithm='logistic', 54 | cv_dict={'type': 'kfolds','n_folds': 5, 'subject_id':subject_id}) 55 | 56 | ######################################################################### 57 | # Ridge classification 58 | ridge_stats = dat.predict(algorithm='ridgeClassifier', 59 | cv_dict={'type': 'kfolds','n_folds': 5, 'subject_id':subject_id}) 60 | 61 | ######################################################################### 62 | # ROC Analyses 63 | # ------------ 64 | # 65 | # We are often interested in evaluating how well a pattern can discriminate 66 | # between different classes of data. However, accuracy could be high because 67 | # of a highly sensitive but not specific model. Receiver operator characteristic 68 | # curves allow us to evaluate the sensitivity and specificity of the model. 69 | # and evaluate how well it can discriminate between high and low pain using 70 | # We use the Roc class to initialize an Roc object and the plot() and summary() 71 | # methods to run the analyses. We could also just run the calculate() method 72 | # to run the analysis without plotting. 73 | 74 | from nltools.analysis import Roc 75 | 76 | roc = Roc(input_values=svm_stats['dist_from_hyperplane_xval'], 77 | binary_outcome=svm_stats['Y'].astype(bool)) 78 | roc.plot() 79 | roc.summary() 80 | 81 | ######################################################################### 82 | # The above example uses single-interval classification, which attempts to 83 | # determine the optimal classification interval. However, sometimes we are 84 | # intersted in directly comparing responses to two images within the same person. 85 | # In this situation we should use forced-choice classification, which looks at 86 | # the relative classification accuracy between two images. You must pass a list 87 | # indicating the ids of each unique subject. 88 | 89 | roc_fc = Roc(input_values=svm_stats['dist_from_hyperplane_xval'], 90 | binary_outcome=svm_stats['Y'].astype(bool), forced_choice=subject_id) 91 | roc_fc.plot() 92 | roc_fc.summary() 93 | 94 | -------------------------------------------------------------------------------- /examples/02_Analysis/plot_multivariate_prediction.py: -------------------------------------------------------------------------------- 1 | """ 2 | Multivariate Prediction 3 | ======================= 4 | 5 | Running MVPA style analyses using multivariate regression is even easier and faster 6 | than univariate methods. All you need to do is specify the algorithm and 7 | cross-validation parameters. Currently, we have several different linear algorithms 8 | implemented from `scikit-learn `_. 9 | 10 | """ 11 | 12 | ######################################################################### 13 | # Load Data 14 | # --------- 15 | # 16 | # First, let's load the pain data for this example. We need to specify the 17 | # training levels. We will grab the pain intensity variable from the data.X 18 | # field. 19 | 20 | from nltools.datasets import fetch_pain 21 | 22 | data = fetch_pain() 23 | data.Y = data.X['PainLevel'] 24 | 25 | ######################################################################### 26 | # Prediction with Cross-Validation 27 | # -------------------------------- 28 | # 29 | # We can now predict the output variable is a dictionary of the most 30 | # useful output from the prediction analyses. The predict function runs 31 | # the prediction multiple times. One of the iterations uses all of the 32 | # data to calculate the 'weight_map'. The other iterations are to estimate 33 | # the cross-validated predictive accuracy. 34 | 35 | stats = data.predict(algorithm='ridge', 36 | cv_dict={'type': 'kfolds','n_folds': 5,'stratified':data.Y}) 37 | 38 | ######################################################################### 39 | # Display the available data in the output dictionary 40 | 41 | stats.keys() 42 | 43 | ######################################################################### 44 | # Plot the multivariate weight map 45 | 46 | stats['weight_map'].plot() 47 | 48 | ######################################################################### 49 | # Return the cross-validated predicted data 50 | 51 | stats['yfit_xval'] 52 | 53 | ######################################################################### 54 | # Algorithms 55 | # ---------- 56 | # 57 | # There are several types of linear algorithms implemented including: 58 | # Support Vector Machines (svr), Principal Components Analysis (pcr), and 59 | # penalized methods such as ridge and lasso. These examples use 5-fold 60 | # cross-validation holding out the same subject in each fold. 61 | 62 | subject_id = data.X['SubjectID'] 63 | svr_stats = data.predict(algorithm='svr', 64 | cv_dict={'type': 'kfolds','n_folds': 5, 65 | 'subject_id':subject_id}, **{'kernel':"linear"}) 66 | 67 | ######################################################################### 68 | # Lasso Regression 69 | 70 | lasso_stats = data.predict(algorithm='lasso', 71 | cv_dict={'type': 'kfolds','n_folds': 5, 72 | 'subject_id':subject_id}, **{'alpha':.1}) 73 | 74 | ######################################################################### 75 | # Principal Components Regression 76 | pcr_stats = data.predict(algorithm='pcr', 77 | cv_dict={'type': 'kfolds','n_folds': 5, 78 | 'subject_id':subject_id}) 79 | 80 | ######################################################################### 81 | # Principal Components Regression with Lasso 82 | 83 | pcr_stats = data.predict(algorithm='lassopcr', 84 | cv_dict={'type': 'kfolds','n_folds': 5, 85 | 'subject_id':subject_id}) 86 | 87 | ######################################################################### 88 | # Cross-Validation Schemes 89 | # ------------------------ 90 | # 91 | # There are several different ways to perform cross-validation. The standard 92 | # approach is to use k-folds, where the data is equally divided into k subsets 93 | # and each fold serves as both training and test. 94 | # Often we want to hold out the same subjects in each fold. 95 | # This can be done by passing in a vector of unique subject IDs that 96 | # correspond to the images in the data frame. 97 | 98 | subject_id = data.X['SubjectID'] 99 | ridge_stats = data.predict(algorithm='ridge', 100 | cv_dict={'type': 'kfolds','n_folds': 5,'subject_id':subject_id}, 101 | plot=False, **{'alpha':.1}) 102 | 103 | ######################################################################### 104 | # Sometimes we want to ensure that the training labels are balanced across 105 | # folds. This can be done using the stratified k-folds method. 106 | 107 | ridge_stats = data.predict(algorithm='ridge', 108 | cv_dict={'type': 'kfolds','n_folds': 5, 'stratified':data.Y}, 109 | plot=False, **{'alpha':.1}) 110 | 111 | ######################################################################### 112 | # Leave One Subject Out Cross-Validaiton (LOSO) is when k=n subjects. 113 | # This can be performed by passing in a vector indicating subject id's of 114 | # each image and using the loso flag. 115 | 116 | ridge_stats = data.predict(algorithm='ridge', 117 | cv_dict={'type': 'loso','subject_id': subject_id}, 118 | plot=False, **{'alpha':.1}) 119 | 120 | ######################################################################### 121 | # There are also methods to estimate the shrinkage parameter for the 122 | # penalized methods using nested crossvalidation with the 123 | # ridgeCV and lassoCV algorithms. 124 | 125 | import numpy as np 126 | 127 | ridgecv_stats = data.predict(algorithm='ridgeCV', 128 | cv_dict={'type': 'kfolds','n_folds': 5, 'stratified':data.Y}, 129 | plot=False, **{'alphas':np.linspace(.1, 10, 5)}) 130 | -------------------------------------------------------------------------------- /examples/02_Analysis/plot_similarity_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Similarity and Distance 3 | ======================= 4 | 5 | This tutorial illustrates how to calculate similarity and distance between images. 6 | 7 | """ 8 | 9 | ######################################################################### 10 | # Load Data 11 | # --------- 12 | # 13 | # First, let's load the pain data for this example. 14 | 15 | from nltools.datasets import fetch_pain 16 | 17 | data = fetch_pain() 18 | 19 | ######################################################################### 20 | # Distance 21 | # -------- 22 | # 23 | # We can calculate the pairwise spatial distance between all images in a Brain_Data() 24 | # instance using any method from sklearn or scipy. This outputs an Adjacency() class 25 | # object. 26 | 27 | d = data.distance(metric='correlation') 28 | d.plot() 29 | 30 | ######################################################################### 31 | # Similarity 32 | # ---------- 33 | # 34 | # The similarity of an image to other images can be computed using the similarity() 35 | # method. Here we calculate the mean image for high pain intensity across all participants 36 | # and calculate the degree of spatial similarity between this image and all pain intensities 37 | # for all participants. This is a useful method for calculating pattern responses. 38 | 39 | import numpy as np 40 | import matplotlib.pylab as plt 41 | 42 | high = data[np.where(data.X['PainLevel']==3)[0]].mean() 43 | r = high.similarity(data, method='correlation') 44 | 45 | f,a = plt.subplots(ncols=2, figsize=(10,4)) 46 | a[0].hist(r) 47 | a[0].set_ylabel('Spatial Similarity') 48 | a[0].set_xlabel('Pain Intensity') 49 | a[0].set_title('Histogram of similarity with mean high intensity image') 50 | a[1].scatter(data.X['PainLevel'],r) 51 | a[1].set_ylabel('Spatial Similarity') 52 | a[1].set_xlabel('Pain Intensity') 53 | a[1].set_title('Spatial Similarity by Pain Intensity') 54 | 55 | -------------------------------------------------------------------------------- /examples/02_Analysis/plot_univariate_regression.py: -------------------------------------------------------------------------------- 1 | """ 2 | Univariate Regression 3 | ===================== 4 | 5 | This example simulates data according to a very simple sketch of brain 6 | imaging data and applies a standard two-level univariate GLM to identify 7 | significant voxels. 8 | 9 | """ 10 | 11 | ######################################################################### 12 | # Download pain dataset from neurovault 13 | # ------------------------------------- 14 | # 15 | # Here we fetch the pain dataset used in Chang et al., 2015. In this dataset 16 | # there are 28 subjects with 3 separate beta images reflecting varying intensities 17 | # of thermal pain (i.e., high, medium, low). The data will be downloaded to ~/nilearn_data, 18 | # and automatically loaded as a Brain_Data() instance. The metadata will be stored in data.X. 19 | 20 | from nltools.datasets import fetch_pain 21 | 22 | data = fetch_pain() 23 | metadata = data.X.copy() 24 | subject_id = metadata['SubjectID'] 25 | 26 | ######################################################################### 27 | # Run Univariate Regression 28 | # ------------------------- 29 | # 30 | # We can loop over subjects and predict the intensity of each voxel from a 31 | # simple model of pain intensity and an intercept. This is just for illustration 32 | # purposes as there are only 3 observations per subject. We initialize an empty 33 | # Brain_Data() instance and loop over all subjects running a univariate regression 34 | # separately for each participant. We aggregate the beta estimates for pain intensity 35 | # across subjects. 36 | 37 | from nltools.data import Brain_Data 38 | import numpy as np 39 | import pandas as pd 40 | 41 | all_sub = Brain_Data() 42 | for s in subject_id.unique(): 43 | sdat = data[np.where(metadata['SubjectID']==s)[0]] 44 | sdat.X = pd.DataFrame(data={'Intercept':np.ones(sdat.shape()[0]),'Pain':sdat.X['PainLevel']}) 45 | stats = sdat.regress() 46 | all_sub = all_sub.append(stats['beta'][1]) 47 | 48 | ######################################################################### 49 | # We can now run a one-sample t-test at every voxel to test whether it is 50 | # significantly different from zero across participants. We will threshold 51 | # the results using FDR correction, q < 0.001. 52 | 53 | t_stats = all_sub.ttest(threshold_dict={'fdr':.001}) 54 | t_stats['thr_t'].plot() 55 | 56 | ######################################################################### 57 | # Run Linear Contrast 58 | # ------------------- 59 | # 60 | # Obviously, the univariate regression isn't a great idea when there are only 61 | # three observations per subject. As we predict a monotonic increase in pain 62 | # across pain intensities, we can also calculate a linear contrast c=(-1,0,1). 63 | # This is simple using matrix multiplication on the centered pain intensity values. 64 | 65 | all_sub = [] 66 | for sub in subject_id.unique(): 67 | sdat = data[metadata['SubjectID']==sub] 68 | sdat.X = pd.DataFrame(data={'Pain':sdat.X['PainLevel']}) 69 | all_sub.append(sdat * np.array(sdat.X['Pain'] - 2)) 70 | all_sub = Brain_Data(all_sub) 71 | 72 | ######################################################################### 73 | # We can again run a one-sample t-test at every voxel using an FDR threshold 74 | # of q < 0.001. 75 | 76 | t_stats = all_sub.ttest(threshold_dict={'fdr':.001}) 77 | t_stats['thr_t'].plot() 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /examples/README.txt: -------------------------------------------------------------------------------- 1 | NLTools Tutorials, Resources, and Community 2 | =========================================== 3 | 4 | 5 | 6 | 1. Comprehensive Courses 7 | --------------------- 8 | 9 | For detailed examples of ``nltools`` functionality you can check out the 10 | following online courses that make use of the toolbox. Both courses use `Hypothesis `_ which is a service that allows anyone to annotate webpages. This allows you to read and post questions and notes that other users can see on each course website. You can do this by clicking on the chevron ("<") on the top right of each site: 11 | 12 | `DartBrains `_ 13 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 14 | A fundmentals of neuroimaging undergraduate level course 15 | 16 | `Naturalistic Neuroimaging Analysis Methods `_ 17 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 18 | A more advanced neuroimaging data analysis course for working with `naturalistic` neuroimaging datasets (e.g. watching tv, movies, playing games, etc) 19 | 20 | 2. Community 21 | --------- 22 | 23 | `Discourse Community `_ 24 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 25 | A Stack Overflow like forum where you can view, contribute, and vote on FAQs regarding ``nltools`` usage. Please ask questions here first so other users can benefit from the answers! 26 | 27 | `Open a Github issue `_ 28 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 29 | For all code related problems 30 | 31 | 32 | 3. Basic Usage Examples 33 | -------------------- 34 | 35 | Check out more basic tutorials below: 36 | 37 | -------------------------------------------------------------------------------- /nltools/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "data", 3 | "datasets", 4 | "analysis", 5 | "cross_validation", 6 | "plotting", 7 | "stats", 8 | "utils", 9 | "file_reader", 10 | "mask", 11 | "prefs", 12 | "external", 13 | "prefs", 14 | "__version__", 15 | ] 16 | 17 | from .analysis import Roc 18 | from .cross_validation import set_cv 19 | from .data import Brain_Data, Adjacency, Groupby, Design_Matrix, Design_Matrix_Series 20 | from .simulator import Simulator 21 | from .prefs import MNI_Template, resolve_mni_path 22 | from .version import __version__ 23 | from .mask import expand_mask, collapse_mask, create_sphere 24 | from .external import SRM, DetSRM 25 | -------------------------------------------------------------------------------- /nltools/analysis.py: -------------------------------------------------------------------------------- 1 | """ 2 | NeuroLearn Analysis Tools 3 | ========================= 4 | These tools provide the ability to quickly run 5 | machine-learning analyses on imaging data 6 | """ 7 | 8 | __all__ = ["Roc"] 9 | __author__ = ["Luke Chang"] 10 | __license__ = "MIT" 11 | 12 | import pandas as pd 13 | import numpy as np 14 | from nltools.plotting import roc_plot 15 | from scipy.stats import norm, binomtest 16 | from sklearn.metrics import auc 17 | from copy import deepcopy 18 | 19 | 20 | class Roc(object): 21 | """Roc Class 22 | 23 | The Roc class is based on Tor Wager's Matlab roc_plot.m function and 24 | allows a user to easily run different types of receiver operator 25 | characteristic curves. For example, one might be interested in single 26 | interval or forced choice. 27 | 28 | Args: 29 | input_values: nibabel data instance 30 | binary_outcome: vector of training labels 31 | threshold_type: ['optimal_overall', 'optimal_balanced', 32 | 'minimum_sdt_bias'] 33 | **kwargs: Additional keyword arguments to pass to the prediction 34 | algorithm 35 | 36 | """ 37 | 38 | def __init__( 39 | self, 40 | input_values=None, 41 | binary_outcome=None, 42 | threshold_type="optimal_overall", 43 | forced_choice=None, 44 | **kwargs 45 | ): 46 | if len(input_values) != len(binary_outcome): 47 | raise ValueError( 48 | "Data Problem: input_value and binary_outcome" "are different lengths." 49 | ) 50 | 51 | if not any(binary_outcome): 52 | raise ValueError("Data Problem: binary_outcome may not be boolean") 53 | 54 | thr_type = ["optimal_overall", "optimal_balanced", "minimum_sdt_bias"] 55 | if threshold_type not in thr_type: 56 | raise ValueError( 57 | "threshold_type must be ['optimal_overall', " 58 | "'optimal_balanced','minimum_sdt_bias']" 59 | ) 60 | 61 | self.input_values = deepcopy(input_values) 62 | self.binary_outcome = deepcopy(binary_outcome) 63 | self.threshold_type = deepcopy(threshold_type) 64 | self.forced_choice = deepcopy(forced_choice) 65 | if isinstance(self.binary_outcome, pd.DataFrame): 66 | self.binary_outcome = np.array(self.binary_outcome).flatten() 67 | else: 68 | self.binary_outcome = deepcopy(binary_outcome) 69 | 70 | def calculate( 71 | self, 72 | input_values=None, 73 | binary_outcome=None, 74 | criterion_values=None, 75 | threshold_type="optimal_overall", 76 | forced_choice=None, 77 | balanced_acc=False, 78 | ): 79 | """Calculate Receiver Operating Characteristic plot (ROC) for 80 | single-interval classification. 81 | 82 | Args: 83 | input_values: nibabel data instance 84 | binary_outcome: vector of training labels 85 | criterion_values: (optional) criterion values for calculating fpr 86 | & tpr 87 | threshold_type: ['optimal_overall', 'optimal_balanced', 88 | 'minimum_sdt_bias'] 89 | forced_choice: index indicating position for each unique subject 90 | (default=None) 91 | balanced_acc: balanced accuracy for single-interval classification 92 | (bool). THIS IS NOT COMPLETELY IMPLEMENTED BECAUSE 93 | IT AFFECTS ACCURACY ESTIMATES, BUT NOT P-VALUES OR 94 | THRESHOLD AT WHICH TO EVALUATE SENS/SPEC 95 | **kwargs: Additional keyword arguments to pass to the prediction 96 | algorithm 97 | 98 | """ 99 | 100 | if input_values is not None: 101 | self.input_values = deepcopy(input_values) 102 | 103 | if binary_outcome is not None: 104 | self.binary_outcome = deepcopy(binary_outcome) 105 | 106 | # Create Criterion Values 107 | if criterion_values is not None: 108 | self.criterion_values = deepcopy(criterion_values) 109 | else: 110 | self.criterion_values = np.linspace( 111 | np.min(self.input_values.squeeze()), 112 | np.max(self.input_values.squeeze()), 113 | num=50 * len(self.binary_outcome), 114 | ) 115 | 116 | if forced_choice is not None: 117 | self.forced_choice = deepcopy(forced_choice) 118 | 119 | if self.forced_choice is not None: 120 | sub_idx = np.unique(self.forced_choice) 121 | if len(sub_idx) != len(self.binary_outcome) / 2: 122 | raise ValueError( 123 | "Make sure that subject ids are correct for 'forced_choice'." 124 | ) 125 | if len( 126 | set(sub_idx).union( 127 | set(np.array(self.forced_choice)[self.binary_outcome]) 128 | ) 129 | ) != len(sub_idx): 130 | raise ValueError("Issue with forced_choice subject labels.") 131 | if len( 132 | set(sub_idx).union( 133 | set(np.array(self.forced_choice)[~self.binary_outcome]) 134 | ) 135 | ) != len(sub_idx): 136 | raise ValueError("Issue with forced_choice subject labels.") 137 | for sub in sub_idx: 138 | sub_mn = ( 139 | self.input_values[ 140 | (self.forced_choice == sub) & (self.binary_outcome) 141 | ] 142 | + self.input_values[ 143 | (self.forced_choice == sub) & (~self.binary_outcome) 144 | ] 145 | )[0] / 2 146 | self.input_values[ 147 | (self.forced_choice == sub) & (self.binary_outcome) 148 | ] = ( 149 | self.input_values[ 150 | (self.forced_choice == sub) & (self.binary_outcome) 151 | ][0] 152 | - sub_mn 153 | ) 154 | self.input_values[ 155 | (self.forced_choice == sub) & (~self.binary_outcome) 156 | ] = ( 157 | self.input_values[ 158 | (self.forced_choice == sub) & (~self.binary_outcome) 159 | ][0] 160 | - sub_mn 161 | ) 162 | self.class_thr = 0 163 | 164 | # Calculate true positive and false positive rate 165 | self.tpr = np.zeros(self.criterion_values.shape) 166 | self.fpr = np.zeros(self.criterion_values.shape) 167 | for i, x in enumerate(self.criterion_values): 168 | wh = self.input_values >= x 169 | self.tpr[i] = np.sum(wh[self.binary_outcome]) / np.sum(self.binary_outcome) 170 | self.fpr[i] = np.sum(wh[~self.binary_outcome]) / np.sum( 171 | ~self.binary_outcome 172 | ) 173 | self.n_true = np.sum(self.binary_outcome) 174 | self.n_false = np.sum(~self.binary_outcome) 175 | self.auc = auc(self.fpr, self.tpr) 176 | 177 | # Get criterion threshold 178 | if self.forced_choice is None: 179 | self.threshold_type = threshold_type 180 | if threshold_type == "optimal_balanced": 181 | mn = (self.tpr + self.fpr) / 2 182 | self.class_thr = self.criterion_values[np.argmax(mn)] 183 | elif threshold_type == "optimal_overall": 184 | n_corr_t = self.tpr * self.n_true 185 | n_corr_f = (1 - self.fpr) * self.n_false 186 | sm = n_corr_t + n_corr_f 187 | self.class_thr = self.criterion_values[np.argmax(sm)] 188 | elif threshold_type == "minimum_sdt_bias": 189 | # Calculate MacMillan and Creelman 2005 Response Bias (c_bias) 190 | c_bias = ( 191 | norm.ppf(np.maximum(0.0001, np.minimum(0.9999, self.tpr))) 192 | + norm.ppf(np.maximum(0.0001, np.minimum(0.9999, self.fpr))) 193 | ) / float(2) 194 | self.class_thr = self.criterion_values[np.argmin(abs(c_bias))] 195 | 196 | # Calculate output 197 | self.false_positive = (self.input_values >= self.class_thr) & ( 198 | ~self.binary_outcome 199 | ) 200 | self.false_negative = (self.input_values < self.class_thr) & ( 201 | self.binary_outcome 202 | ) 203 | self.misclass = (self.false_negative) | (self.false_positive) 204 | self.true_positive = (self.binary_outcome) & (~self.misclass) 205 | self.true_negative = (~self.binary_outcome) & (~self.misclass) 206 | self.sensitivity = ( 207 | np.sum(self.input_values[self.binary_outcome] >= self.class_thr) 208 | / self.n_true 209 | ) 210 | self.specificity = ( 211 | 1 212 | - np.sum(self.input_values[~self.binary_outcome] >= self.class_thr) 213 | / self.n_false 214 | ) 215 | self.ppv = np.sum(self.true_positive) / ( 216 | np.sum(self.true_positive) + np.sum(self.false_positive) 217 | ) 218 | if self.forced_choice is not None: 219 | self.true_positive = self.true_positive[self.binary_outcome] 220 | self.true_negative = self.true_negative[~self.binary_outcome] 221 | self.false_negative = self.false_negative[self.binary_outcome] 222 | self.false_positive = self.false_positive[~self.binary_outcome] 223 | self.misclass = (self.false_positive) | (self.false_negative) 224 | 225 | # Calculate Accuracy 226 | if balanced_acc: 227 | self.accuracy = np.mean( 228 | [self.sensitivity, self.specificity] 229 | ) # See Brodersen, Ong, Stephan, Buhmann (2010) 230 | else: 231 | self.accuracy = 1 - np.mean(self.misclass) 232 | 233 | # Calculate p-Value using binomial test (can add hierarchical version of binomial test) 234 | self.n = len(self.misclass) 235 | self.accuracy_p = binomtest(int(np.sum(~self.misclass)), self.n, p=0.5) 236 | self.accuracy_se = np.sqrt( 237 | np.mean(~self.misclass) * (np.mean(~self.misclass)) / self.n 238 | ) 239 | 240 | def plot(self, plot_method="gaussian", balanced_acc=False, **kwargs): 241 | """Create ROC Plot 242 | 243 | Create a specific kind of ROC curve plot, based on input values 244 | along a continuous distribution and a binary outcome variable (logical) 245 | 246 | Args: 247 | plot_method: type of plot ['gaussian','observed'] 248 | binary_outcome: vector of training labels 249 | **kwargs: Additional keyword arguments to pass to the prediction 250 | algorithm 251 | 252 | Returns: 253 | fig 254 | 255 | """ 256 | 257 | self.calculate(balanced_acc=balanced_acc) # Calculate ROC parameters 258 | 259 | if plot_method == "gaussian": 260 | if self.forced_choice is not None: 261 | sub_idx = np.unique(self.forced_choice) 262 | diff_scores = [] 263 | for sub in sub_idx: 264 | diff_scores.append( 265 | self.input_values[ 266 | (self.forced_choice == sub) & (self.binary_outcome) 267 | ][0] 268 | - self.input_values[ 269 | (self.forced_choice == sub) & (~self.binary_outcome) 270 | ][0] 271 | ) 272 | diff_scores = np.array(diff_scores) 273 | mn_diff = np.mean(diff_scores) 274 | d = mn_diff / np.std(diff_scores) 275 | pooled_sd = np.std(diff_scores) / np.sqrt(2) 276 | d_a_model = mn_diff / pooled_sd 277 | 278 | expected_acc = 1 - norm.cdf(0, d, 1) 279 | self.sensitivity = expected_acc 280 | self.specificity = expected_acc 281 | self.ppv = self.sensitivity / (self.sensitivity + 1 - self.specificity) 282 | self.auc = norm.cdf(d_a_model / np.sqrt(2)) 283 | 284 | x = np.arange(-3, 3, 0.1) 285 | self.tpr_smooth = 1 - norm.cdf(x, d, 1) 286 | self.fpr_smooth = 1 - norm.cdf(x, -d, 1) 287 | else: 288 | mn_true = np.mean(self.input_values[self.binary_outcome]) 289 | mn_false = np.mean(self.input_values[~self.binary_outcome]) 290 | var_true = np.var(self.input_values[self.binary_outcome]) 291 | var_false = np.var(self.input_values[~self.binary_outcome]) 292 | pooled_sd = np.sqrt( 293 | (var_true * (self.n_true - 1)) / (self.n_true + self.n_false - 2) 294 | ) 295 | d = (mn_true - mn_false) / pooled_sd 296 | z_true = mn_true / pooled_sd 297 | z_false = mn_false / pooled_sd 298 | 299 | x = np.arange(z_false - 3, z_true + 3, 0.1) 300 | self.tpr_smooth = 1 - (norm.cdf(x, z_true, 1)) 301 | self.fpr_smooth = 1 - (norm.cdf(x, z_false, 1)) 302 | 303 | self.aucn = auc(self.fpr_smooth, self.tpr_smooth) 304 | fig = roc_plot(self.fpr_smooth, self.tpr_smooth) 305 | 306 | elif plot_method == "observed": 307 | fig = roc_plot(self.fpr, self.tpr) 308 | else: 309 | raise ValueError("plot_method must be 'gaussian' or 'observed'") 310 | return fig 311 | 312 | def summary(self): 313 | """Display a formatted summary of ROC analysis.""" 314 | 315 | print("------------------------") 316 | print(".:ROC Analysis Summary:.") 317 | print("------------------------") 318 | print("{:20s}".format("Accuracy:") + "{:.2f}".format(self.accuracy)) 319 | print("{:20s}".format("Accuracy SE:") + "{:.2f}".format(self.accuracy_se)) 320 | print( 321 | "{:20s}".format("Accuracy p-value:") 322 | + "{:.2f}".format(self.accuracy_p.pvalue) 323 | ) 324 | print("{:20s}".format("Sensitivity:") + "{:.2f}".format(self.sensitivity)) 325 | print("{:20s}".format("Specificity:") + "{:.2f}".format(self.specificity)) 326 | print("{:20s}".format("AUC:") + "{:.2f}".format(self.auc)) 327 | print("{:20s}".format("PPV:") + "{:.2f}".format(self.ppv)) 328 | print("------------------------") 329 | -------------------------------------------------------------------------------- /nltools/cross_validation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cross-Validation Data Classes 3 | ============================= 4 | 5 | Scikit-learn compatible classes for performing various 6 | types of cross-validation 7 | 8 | """ 9 | 10 | __all__ = ["KFoldStratified", "set_cv"] 11 | __author__ = ["Luke Chang"] 12 | __license__ = "MIT" 13 | 14 | from sklearn.model_selection._split import _BaseKFold 15 | from sklearn.utils.validation import check_array 16 | import numpy as np 17 | import pandas as pd 18 | 19 | 20 | class KFoldStratified(_BaseKFold): 21 | """K-Folds cross validation iterator which stratifies continuous data 22 | (unlike scikit-learn equivalent). 23 | 24 | Provides train/test indices to split data in train test sets. Split 25 | dataset into k consecutive folds while ensuring that same subject is 26 | held out within each fold. Each fold is then used a validation set 27 | once while the k - 1 remaining folds form the training set. 28 | Extension of KFold from scikit-learn cross_validation model 29 | 30 | Args: 31 | n_splits: int, default=3 32 | Number of folds. Must be at least 2. 33 | shuffle: boolean, optional 34 | Whether to shuffle the data before splitting into batches. 35 | random_state: None, int or RandomState 36 | Pseudo-random number generator state used for random 37 | sampling. If None, use default numpy RNG for shuffling 38 | 39 | """ 40 | 41 | def __init__(self, n_splits=3, shuffle=False, random_state=None): 42 | super(KFoldStratified, self).__init__( 43 | n_splits=n_splits, shuffle=shuffle, random_state=random_state 44 | ) 45 | 46 | def _make_test_folds(self, X, y=None, groups=None): 47 | y = pd.DataFrame(y) 48 | y_sort = y.sort_values(0) 49 | test_folds = np.nan * np.ones(len(y_sort)) 50 | for k in range(self.n_splits): 51 | test_idx = y_sort.index[np.arange(k, len(y_sort), self.n_splits)] 52 | test_folds[y_sort.iloc[test_idx].index] = k 53 | return test_folds 54 | 55 | def _iter_test_masks(self, X, y=None, groups=None): 56 | test_folds = self._make_test_folds(X, y) 57 | for i in range(self.n_splits): 58 | yield test_folds == i 59 | 60 | def split(self, X, y, groups=None): 61 | """Generate indices to split data into training and test set. 62 | 63 | Args: 64 | X : array-like, shape (n_samples, n_features) 65 | Training data, where n_samples is the number of samples 66 | and n_features is the number of features. 67 | Note that providing ``y`` is sufficient to generate the splits 68 | and hence ``np.zeros(n_samples)`` may be used as a placeholder 69 | for ``X`` instead of actual training data. 70 | y : array-like, shape (n_samples,) 71 | The target variable for supervised learning problems. 72 | Stratification is done based on the y labels. 73 | groups : (object) Always ignored, exists for compatibility. 74 | 75 | Returns: 76 | train : (ndarray) The training set indices for that split. 77 | test : (ndarray) The testing set indices for that split. 78 | 79 | """ 80 | y = check_array(y, ensure_2d=False, dtype=None) 81 | return super(KFoldStratified, self).split(X, y, groups) 82 | 83 | 84 | def set_cv(Y=None, cv_dict=None, return_generator=True): 85 | """Helper function to create a sci-kit learn compatible cv object using 86 | common parameters for prediction analyses. 87 | 88 | Args: 89 | Y: (pd.DataFrame) Pandas Dataframe of Y labels 90 | cv_dict: (dict) Type of cross_validation to use. A dictionary of 91 | {'type': 'kfolds', 'n_folds': n}, 92 | {'type': 'kfolds', 'n_folds': n, 'stratified': Y}, 93 | {'type': 'kfolds', 'n_folds': n, 'subject_id': holdout}, or 94 | {'type': 'loso', 'subject_id': holdout} 95 | return_generator (bool): return a cv generator instead of an instance; default True 96 | Returns: 97 | cv: a scikit-learn model-selection generator 98 | 99 | """ 100 | 101 | if isinstance(cv_dict, dict): 102 | if cv_dict["type"] == "kfolds": 103 | if "subject_id" in cv_dict: # Hold out subjects within each fold 104 | from sklearn.model_selection import GroupKFold 105 | 106 | cv_inst = GroupKFold(n_splits=cv_dict["n_folds"]) 107 | cv = cv_inst.split( 108 | X=np.zeros(len(Y)), y=Y, groups=cv_dict["subject_id"] 109 | ) 110 | elif "stratified" in cv_dict: # Stratified K-Folds Continuous 111 | from nltools.cross_validation import KFoldStratified 112 | 113 | cv_inst = KFoldStratified(n_splits=cv_dict["n_folds"]) 114 | cv = cv_inst.split(X=np.zeros(len(Y)), y=Y) 115 | else: # Normal K-Folds 116 | from sklearn.model_selection import KFold 117 | 118 | cv_inst = KFold(n_splits=cv_dict["n_folds"]) 119 | cv = cv_inst.split(X=np.zeros(len(Y)), y=Y) 120 | elif cv_dict["type"] == "loso": # Leave One Subject Out 121 | from sklearn.model_selection import LeaveOneGroupOut 122 | 123 | cv_inst = LeaveOneGroupOut() 124 | cv = cv_inst.split(X=np.zeros(len(Y)), y=Y, groups=cv_dict["subject_id"]) 125 | else: 126 | raise ValueError( 127 | """Make sure you specify a dictionary of 128 | {'type': 'kfolds', 'n_folds': n}, 129 | {'type': 'kfolds', 'n_folds': n, 'stratified': Y}, 130 | {'type': 'kfolds', 'n_folds': n, 131 | 'subject_id': holdout}, or {'type': 'loso', 132 | 'subject_id': holdout}, where n = number of folds, 133 | and subject = vector of subject ids that 134 | corresponds to self.Y""" 135 | ) 136 | else: 137 | raise ValueError("Make sure 'cv_dict' is a dictionary.") 138 | if return_generator: 139 | return cv 140 | else: 141 | return cv_inst 142 | -------------------------------------------------------------------------------- /nltools/data/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | nltools data types. 3 | """ 4 | 5 | from .brain_data import Brain_Data, Groupby 6 | from .adjacency import Adjacency 7 | from .design_matrix import Design_Matrix, Design_Matrix_Series 8 | 9 | __all__ = [ 10 | "Brain_Data", 11 | "Adjacency", 12 | "Groupby", 13 | "Design_Matrix", 14 | "Design_Matrix_Series", 15 | ] 16 | -------------------------------------------------------------------------------- /nltools/datasets.py: -------------------------------------------------------------------------------- 1 | """ 2 | NeuroLearn datasets 3 | =================== 4 | 5 | functions to help download datasets 6 | 7 | """ 8 | 9 | ## Notes: 10 | # Need to figure out how to speed up loading and resampling of data 11 | 12 | __all__ = [ 13 | "download_nifti", 14 | "get_collection_image_metadata", 15 | "download_collection", 16 | "fetch_emotion_ratings", 17 | "fetch_pain", 18 | ] 19 | __author__ = ["Luke Chang"] 20 | __license__ = "MIT" 21 | 22 | import os 23 | import pandas as pd 24 | from nltools.data import Brain_Data 25 | from nilearn.datasets.utils import _get_dataset_dir, _fetch_file 26 | from pynv import Client 27 | 28 | # Optional dependencies 29 | try: 30 | import requests 31 | except ImportError: 32 | pass 33 | 34 | 35 | def download_nifti(url, data_dir=None): 36 | """Download a image to a nifti file.""" 37 | local_filename = url.split("/")[-1] 38 | if data_dir is not None: 39 | if not os.path.isdir(data_dir): 40 | os.makedirs(data_dir) 41 | local_filename = os.path.join(data_dir, local_filename) 42 | r = requests.get(url, stream=True) 43 | with open(local_filename, "wb") as f: 44 | for chunk in r.iter_content(chunk_size=1024): 45 | if chunk: # filter out keep-alive new chunks 46 | f.write(chunk) 47 | return local_filename 48 | 49 | 50 | def get_collection_image_metadata(collection=None, data_dir=None, limit=10): 51 | """ 52 | Get image metadata associated with collection 53 | 54 | Args: 55 | collection (int, optional): collection id. Defaults to None. 56 | data_dir (str, optional): data directory. Defaults to None. 57 | limit (int, optional): number of images to increment. Defaults to 10. 58 | 59 | Returns: 60 | pd.DataFrame: Dataframe with full image metadata from collection 61 | """ 62 | 63 | if os.path.isfile(os.path.join(data_dir, "metadata.csv")): 64 | dat = pd.read_csv(os.path.join(data_dir, "metadata.csv")) 65 | else: 66 | offset = 0 67 | api = Client() 68 | i = api.get_collection_images( 69 | collection_id=collection, limit=limit, offset=offset 70 | ) 71 | dat = pd.DataFrame(columns=i["results"][0].keys()) 72 | while int(offset) < int(i["count"]): 73 | for x in i["results"]: 74 | dat = pd.concat([dat, pd.DataFrame(x, index=[0])], ignore_index=True) 75 | offset = offset + limit 76 | i = api.get_collection_images( 77 | collection_id=collection, limit=limit, offset=offset 78 | ) 79 | dat.to_csv(os.path.join(data_dir, "metadata.csv"), index=False) 80 | return dat 81 | 82 | 83 | def download_collection( 84 | collection=None, data_dir=None, overwrite=False, resume=True, verbose=1 85 | ): 86 | """ 87 | Download images and metadata from Neurovault collection 88 | 89 | Args: 90 | collection (int, optional): collection id. Defaults to None. 91 | data_dir (str, optional): data directory. Defaults to None. 92 | overwrite (bool, optional): overwrite data directory. Defaults to False. 93 | resume (bool, optional): resume download. Defaults to True. 94 | verbose (int, optional): print diagnostic messages. Defaults to 1. 95 | 96 | Returns: 97 | (pd.DataFrame, list): (DataFrame of image metadata, list of files from downloaded collection) 98 | """ 99 | 100 | if data_dir is None: 101 | data_dir = _get_dataset_dir(str(collection), data_dir=data_dir, verbose=verbose) 102 | 103 | # Get collection Metadata 104 | metadata = get_collection_image_metadata(collection=collection, data_dir=data_dir) 105 | 106 | # Get images 107 | files = [] 108 | for f in metadata["file"]: 109 | files.append( 110 | _fetch_file( 111 | f, data_dir, resume=resume, verbose=verbose, overwrite=overwrite 112 | ) 113 | ) 114 | 115 | return (metadata, files) 116 | 117 | 118 | def fetch_pain(data_dir=None, resume=True, verbose=1): 119 | """Download and loads pain dataset from neurovault 120 | 121 | Args: 122 | data_dir: (string, optional) Path of the data directory. Used to force data storage in a specified location. Default: None 123 | 124 | Returns: 125 | out: (Brain_Data) Brain_Data object with downloaded data. X=metadata 126 | 127 | """ 128 | 129 | collection = 504 130 | dataset_name = "chang2015_pain" 131 | data_dir = _get_dataset_dir(dataset_name, data_dir=data_dir, verbose=verbose) 132 | metadata, files = download_collection( 133 | collection=collection, data_dir=data_dir, resume=resume, verbose=verbose 134 | ) 135 | return Brain_Data(data=files, X=metadata) 136 | 137 | 138 | def fetch_emotion_ratings(data_dir=None, resume=True, verbose=1): 139 | """Download and loads emotion rating dataset from neurovault 140 | 141 | Args: 142 | data_dir: (string, optional). Path of the data directory. Used to force data storage in a specified location. Default: None 143 | 144 | Returns: 145 | out: (Brain_Data) Brain_Data object with downloaded data. X=metadata 146 | 147 | """ 148 | 149 | collection = 1964 150 | dataset_name = "chang2015_emotion_ratings" 151 | data_dir = _get_dataset_dir(dataset_name, data_dir=data_dir, verbose=verbose) 152 | metadata, files = download_collection( 153 | collection=collection, data_dir=data_dir, resume=resume, verbose=verbose 154 | ) 155 | return Brain_Data(data=files, X=metadata) 156 | -------------------------------------------------------------------------------- /nltools/external/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | External functions 3 | """ 4 | 5 | from .srm import DetSRM, SRM 6 | from .hrf import ( 7 | spm_hrf, 8 | glover_hrf, 9 | spm_time_derivative, 10 | glover_time_derivative, 11 | spm_dispersion_derivative, 12 | ) 13 | -------------------------------------------------------------------------------- /nltools/external/hrf.py: -------------------------------------------------------------------------------- 1 | """ 2 | HRF Functions 3 | ============= 4 | 5 | Various Hemodynamic Response Functions (HRFs) implemented by NiPy 6 | 7 | Copyright (c) 2006-2017, NIPY Developers 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are 12 | met: 13 | 14 | * Redistributions of source code must retain the above copyright 15 | notice, this list of conditions and the following disclaimer. 16 | 17 | * Redistributions in binary form must reproduce the above 18 | copyright notice, this list of conditions and the following 19 | disclaimer in the documentation and/or other materials provided 20 | with the distribution. 21 | 22 | * Neither the name of the NIPY Developers nor the names of any 23 | contributors may be used to endorse or promote products derived 24 | from this software without specific prior written permission. 25 | 26 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 27 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 28 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 29 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 30 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 31 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 32 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 33 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 34 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 35 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 36 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | """ 38 | 39 | __all__ = [ 40 | "spm_hrf", 41 | "glover_hrf", 42 | "spm_time_derivative", 43 | "glover_time_derivative", 44 | "spm_dispersion_derivative", 45 | ] 46 | 47 | from scipy.stats import gamma 48 | import numpy as np 49 | 50 | 51 | def _gamma_difference_hrf( 52 | tr, 53 | oversampling=16, 54 | time_length=32, 55 | onset=0.0, 56 | delay=6, 57 | undershoot=16.0, 58 | dispersion=1.0, 59 | u_dispersion=1.0, 60 | ratio=0.167, 61 | ): 62 | """Compute an hrf as the difference of two gamma functions 63 | Parameters 64 | ---------- 65 | tr: float, scan repeat time, in seconds 66 | oversampling: int, temporal oversampling factor, optional 67 | time_length: int, hrf kernel length, in seconds 68 | onset: float, onset of the hrf 69 | Returns 70 | ------- 71 | hrf: array of shape(length / tr * oversampling, float), 72 | hrf sampling on the oversampled time grid 73 | """ 74 | dt = tr / oversampling 75 | time_stamps = np.linspace(0, time_length, int(time_length / dt)) 76 | time_stamps -= onset / dt 77 | hrf = gamma.pdf( 78 | time_stamps, delay / dispersion, dt / dispersion 79 | ) - ratio * gamma.pdf(time_stamps, undershoot / u_dispersion, dt / u_dispersion) 80 | hrf /= hrf.sum() 81 | return hrf 82 | 83 | 84 | def spm_hrf(tr, oversampling=16, time_length=32.0, onset=0.0): 85 | """Implementation of the SPM hrf model. 86 | 87 | Args: 88 | tr: float, scan repeat time, in seconds 89 | oversampling: int, temporal oversampling factor, optional 90 | time_length: float, hrf kernel length, in seconds 91 | onset: float, onset of the response 92 | 93 | Returns: 94 | hrf: array of shape(length / tr * oversampling, float), 95 | hrf sampling on the oversampled time grid 96 | 97 | """ 98 | 99 | return _gamma_difference_hrf(tr, oversampling, time_length, onset) 100 | 101 | 102 | def glover_hrf(tr, oversampling=16, time_length=32, onset=0.0): 103 | """Implementation of the Glover hrf model. 104 | 105 | Args: 106 | tr: float, scan repeat time, in seconds 107 | oversampling: int, temporal oversampling factor, optional 108 | time_length: int, hrf kernel length, in seconds 109 | onset: float, onset of the response 110 | 111 | Returns: 112 | hrf: array of shape(length / tr * oversampling, float), 113 | hrf sampling on the oversampled time grid 114 | 115 | """ 116 | 117 | return _gamma_difference_hrf( 118 | tr, 119 | oversampling, 120 | time_length, 121 | onset, 122 | delay=6, 123 | undershoot=12.0, 124 | dispersion=0.9, 125 | u_dispersion=0.9, 126 | ratio=0.35, 127 | ) 128 | 129 | 130 | def spm_time_derivative(tr, oversampling=16, time_length=32.0, onset=0.0): 131 | """Implementation of the SPM time derivative hrf (dhrf) model. 132 | 133 | Args: 134 | tr: float, scan repeat time, in seconds 135 | oversampling: int, temporal oversampling factor, optional 136 | time_length: float, hrf kernel length, in seconds 137 | onset: float, onset of the response 138 | 139 | Returns: 140 | dhrf: array of shape(length / tr, float), 141 | dhrf sampling on the provided grid 142 | 143 | """ 144 | 145 | do = 0.1 146 | dhrf = ( 147 | 1.0 148 | / do 149 | * ( 150 | spm_hrf(tr, oversampling, time_length, onset + do) 151 | - spm_hrf(tr, oversampling, time_length, onset) 152 | ) 153 | ) 154 | return dhrf 155 | 156 | 157 | def glover_time_derivative(tr, oversampling=16, time_length=32.0, onset=0.0): 158 | """Implementation of the flover time derivative hrf (dhrf) model. 159 | 160 | Args: 161 | tr: float, scan repeat time, in seconds 162 | oversampling: int, temporal oversampling factor, optional 163 | time_length: float, hrf kernel length, in seconds 164 | onset: float, onset of the response 165 | 166 | Returns: 167 | dhrf: array of shape(length / tr, float), 168 | dhrf sampling on the provided grid 169 | 170 | """ 171 | 172 | do = 0.1 173 | dhrf = ( 174 | 1.0 175 | / do 176 | * ( 177 | glover_hrf(tr, oversampling, time_length, onset + do) 178 | - glover_hrf(tr, oversampling, time_length, onset) 179 | ) 180 | ) 181 | return dhrf 182 | 183 | 184 | def spm_dispersion_derivative(tr, oversampling=16, time_length=32.0, onset=0.0): 185 | """Implementation of the SPM dispersion derivative hrf model. 186 | 187 | Args: 188 | tr: float, scan repeat time, in seconds 189 | oversampling: int, temporal oversampling factor, optional 190 | time_length: float, hrf kernel length, in seconds 191 | onset: float, onset of the response 192 | 193 | Returns: 194 | dhrf: array of shape(length / tr * oversampling, float), 195 | dhrf sampling on the oversampled time grid 196 | 197 | """ 198 | 199 | dd = 0.01 200 | dhrf = ( 201 | 1.0 202 | / dd 203 | * ( 204 | _gamma_difference_hrf( 205 | tr, oversampling, time_length, onset, dispersion=1.0 + dd 206 | ) 207 | - spm_hrf(tr, oversampling, time_length, onset) 208 | ) 209 | ) 210 | return dhrf 211 | -------------------------------------------------------------------------------- /nltools/file_reader.py: -------------------------------------------------------------------------------- 1 | """ 2 | NeuroLearn File Reading Tools 3 | ============================= 4 | 5 | """ 6 | 7 | __all__ = ["onsets_to_dm"] 8 | __author__ = ["Eshin Jolly"] 9 | __license__ = "MIT" 10 | 11 | import pandas as pd 12 | import numpy as np 13 | from nltools.data import Design_Matrix 14 | import warnings 15 | from pathlib import Path 16 | 17 | 18 | def onsets_to_dm( 19 | F, 20 | sampling_freq, 21 | run_length, 22 | header="infer", 23 | sort=False, 24 | keep_separate=True, 25 | add_poly=None, 26 | unique_cols=None, 27 | fill_na=None, 28 | **kwargs, 29 | ): 30 | """ 31 | This function can assist in reading in one or several in a 2-3 column onsets files, specified in seconds and converting it to a Design Matrix organized as samples X Stimulus Classes. sampling_freq should be specified in hertz; for TRs use hertz = 1/TR. Onsets files **must** be organized with columns in one of the following 4 formats: 32 | 33 | 1) 'Stim, Onset' 34 | 2) 'Onset, Stim' 35 | 3) 'Stim, Onset, Duration' 36 | 4) 'Onset, Duration, Stim' 37 | 38 | No other file organizations are currently supported. *Note:* Stimulus offsets (onset + duration) that fall into an adjacent TR include that full TR. E.g. offset of 10.16s with TR = 2 has an offset of TR 5, which spans 10-12s, rather than an offset of TR 4, which spans 8-10s. 39 | 40 | Args: 41 | F (str/Path/pd.DataFrame): filepath or pandas dataframe 42 | sampling_freq (float): samping frequency in hertz, i.e 1 / TR 43 | run_length (int): run length in number of TRs 44 | header (str/None, optional): whether there's an additional header row in the 45 | supplied file/dataframe. See `pd.read_csv` for more details. Defaults to `"infer"`. 46 | sort (bool, optional): whether to sort dataframe columns alphabetically. Defaults to False. 47 | keep_separate (bool, optional): if a list of files or dataframes is supplied, 48 | whether to create separate polynomial columns per file. Defaults to `True`. 49 | add_poly (bool/int, optional): whether to add Nth order polynomials to design 50 | matrix. Defaults to None. 51 | unique_cols (list/None, optional): if a list of files or dataframes is supplied, 52 | what additional columns to keep separate per file (e.g. spikes). Defaults to None. 53 | fill_na (Any, optional): what to replace NaNs with. Defaults to None (no filling). 54 | 55 | 56 | Returns: 57 | nltools.data.Design_Matrix: design matrix organized as TRs x Stims 58 | """ 59 | 60 | if not isinstance(F, list): 61 | F = [F] 62 | 63 | if not isinstance(sampling_freq, (float, np.floating)): 64 | raise TypeError("sampling_freq must be a float") 65 | 66 | out = [] 67 | TR = 1.0 / sampling_freq 68 | for f in F: 69 | if isinstance(f, str) or isinstance(f, Path): 70 | df = pd.read_csv(f, header=header, **kwargs) 71 | elif isinstance(f, pd.core.frame.DataFrame): 72 | df = f.copy() 73 | else: 74 | raise TypeError("Input needs to be file path or pandas dataframe!") 75 | # Keep an unaltered copy of the original dataframe for checking purposes below 76 | data = df.copy() 77 | 78 | if df.shape[1] == 2: 79 | warnings.warn( 80 | "Only 2 columns in file, assuming all stimuli are the same duration" 81 | ) 82 | elif df.shape[1] == 1 or df.shape[1] > 3: 83 | raise ValueError("Can only handle files with 2 or 3 columns!") 84 | 85 | # Try to infer the header 86 | if header is None: 87 | possibleHeaders = ["Stim", "Onset", "Duration"] 88 | if isinstance(df.iloc[0, 0], str): 89 | df.columns = possibleHeaders[: df.shape[1]] 90 | elif isinstance(df.iloc[0, df.shape[1] - 1], str): 91 | df.columns = possibleHeaders[1:] + [possibleHeaders[0]] 92 | else: 93 | raise ValueError( 94 | "Can't figure out onset file organization. Make sure file has no more than 3 columns specified as 'Stim,Onset,Duration' or 'Onset,Duration,Stim'" 95 | ) 96 | 97 | # Compute an offset in seconds if a Duration is provided 98 | if df.shape[1] == 3: 99 | df["Offset"] = df["Onset"] + df["Duration"] 100 | # Onset always starts at the closest TR rounded down, e.g. 101 | # with TR = 2, and onset = 10.1 or 11.7 will both have onset of TR 5 as it spans the window 10-12s 102 | df["Onset"] = df["Onset"].apply(lambda x: int(np.floor(x / TR))) 103 | 104 | # Offset includes the subsequent if Offset falls within window covered by that TR 105 | # but not if it falls exactly on the subsequent TR, e.g. if TR = 2, and offset = 10.16, then TR 5 will be included but if offset = 10.00, TR 5 will not be included, as it covers the window 10-12s 106 | if "Offset" in df.columns: 107 | 108 | def conditional_round(x, TR): 109 | """Conditional rounding to the next TR if offset falls within window, otherwise not""" 110 | dur_in_TRs = x / TR 111 | dur_in_TRs_rounded_down = np.floor(dur_in_TRs) 112 | # If in the future we wanted to enable the ability to include a TR based on a % of that TR we can change the next line to compare to some value, e.g. at least 0.5s into that TR: dur_in_TRs - dur_in_TRs_rounded_down > 0.5 113 | if dur_in_TRs > dur_in_TRs_rounded_down: 114 | return dur_in_TRs_rounded_down 115 | else: 116 | return dur_in_TRs_rounded_down - 1 117 | 118 | # Apply function 119 | df["Offset"] = df["Offset"].apply(conditional_round, args=(TR,)) 120 | 121 | # Build dummy codes 122 | X = Design_Matrix( 123 | np.zeros([run_length, df["Stim"].nunique()]), 124 | columns=df["Stim"].unique(), 125 | sampling_freq=sampling_freq, 126 | ) 127 | for i, row in df.iterrows(): 128 | if "Offset" in df.columns: 129 | X.loc[row["Onset"] : row["Offset"], row["Stim"]] = 1 130 | else: 131 | X.loc[row["Onset"], row["Stim"]] = 1 132 | # DISABLED cause this isn't quite accurate for stimuli of different durations 133 | # Run a check 134 | # if "Offset" in df.columns: 135 | # onsets = X.sum().values 136 | # stim_counts = data.Stim.value_counts(sort=False)[X.columns] 137 | # durations = data.groupby("Stim").Duration.mean().values 138 | # for i, (o, c, d) in enumerate(zip(onsets, stim_counts, durations)): 139 | # if c * (d / TR) <= o <= c * ((d / TR) + 1): 140 | # pass 141 | # else: 142 | # warnings.warn( 143 | # f"Computed onsets for {data.Stim.unique()[i]} are inconsistent ({o}) with expected values ({c * (d / TR)} to {c * ((d / TR) + 1)}). Please manually verify the outputted Design_Matrix!" 144 | # ) 145 | 146 | if sort: 147 | X = X.reindex(sorted(X.columns), axis=1) 148 | 149 | out.append(X) 150 | if len(out) > 1: 151 | if add_poly is not None: 152 | out = [e.add_poly(add_poly) for e in out] 153 | 154 | out_dm = out[0].append( 155 | out[1:], 156 | keep_separate=keep_separate, 157 | unique_cols=unique_cols, 158 | fill_na=fill_na, 159 | ) 160 | else: 161 | out_dm = out[0] 162 | if add_poly is not None: 163 | out_dm = out_dm.add_poly(add_poly) 164 | if fill_na is not None: 165 | out_dm = out_dm.fill_na(fill_na) 166 | 167 | return out_dm 168 | -------------------------------------------------------------------------------- /nltools/mask.py: -------------------------------------------------------------------------------- 1 | """ 2 | NeuroLearn Mask Classes 3 | ======================= 4 | 5 | Classes to represent masks 6 | 7 | """ 8 | 9 | __all__ = ["create_sphere", "expand_mask", "collapse_mask", "roi_to_brain"] 10 | __author__ = ["Luke Chang", "Sam Greydanus"] 11 | __license__ = "MIT" 12 | 13 | import os 14 | import nibabel as nib 15 | from nltools.prefs import MNI_Template, resolve_mni_path 16 | import pandas as pd 17 | import numpy as np 18 | import warnings 19 | from nilearn.masking import intersect_masks 20 | 21 | 22 | def create_sphere(coordinates, radius=5, mask=None): 23 | """Generate a set of spheres in the brain mask space 24 | 25 | Args: 26 | radius: vector of radius. Will create multiple spheres if 27 | len(radius) > 1 28 | centers: a vector of sphere centers of the form [px, py, pz] or 29 | [[px1, py1, pz1], ..., [pxn, pyn, pzn]] 30 | 31 | """ 32 | from nltools.data import Brain_Data 33 | 34 | if mask is not None: 35 | if not isinstance(mask, nib.Nifti1Image): 36 | if isinstance(mask, str): 37 | if os.path.isfile(mask): 38 | mask = nib.load(mask) 39 | else: 40 | raise ValueError( 41 | "mask is not a nibabel instance or a valid " "file name" 42 | ) 43 | 44 | else: 45 | mask = nib.load(resolve_mni_path(MNI_Template)["mask"]) 46 | 47 | def sphere(r, p, mask): 48 | """create a sphere of given radius at some point p in the brain mask 49 | 50 | Args: 51 | r: radius of the sphere 52 | p: point (in coordinates of the brain mask) of the center of the 53 | sphere 54 | 55 | """ 56 | dims = mask.shape 57 | m = [dims[0] / 2, dims[1] / 2, dims[2] / 2] 58 | x, y, z = np.ogrid[ 59 | -m[0] : dims[0] - m[0], -m[1] : dims[1] - m[1], -m[2] : dims[2] - m[2] 60 | ] 61 | mask_r = x * x + y * y + z * z <= r * r 62 | 63 | activation = np.zeros(dims) 64 | activation[mask_r] = 1 65 | translation_affine = np.array( 66 | [ 67 | [1, 0, 0, p[0] - m[0]], 68 | [0, 1, 0, p[1] - m[1]], 69 | [0, 0, 1, p[2] - m[2]], 70 | [0, 0, 0, 1], 71 | ] 72 | ) 73 | 74 | return nib.Nifti1Image(activation, affine=translation_affine) 75 | 76 | if any(isinstance(i, list) for i in coordinates): 77 | if isinstance(radius, list): 78 | if len(radius) != len(coordinates): 79 | raise ValueError( 80 | "Make sure length of radius list matches" 81 | "length of coordinate list." 82 | ) 83 | elif isinstance(radius, int): 84 | radius = [radius] * len(coordinates) 85 | out = Brain_Data( 86 | nib.Nifti1Image(np.zeros_like(mask.get_fdata()), affine=mask.affine), 87 | mask=mask, 88 | ) 89 | for r, c in zip(radius, coordinates): 90 | out = out + Brain_Data(sphere(r, c, mask), mask=mask) 91 | else: 92 | out = Brain_Data(sphere(radius, coordinates, mask), mask=mask) 93 | out = out.to_nifti() 94 | out.get_fdata()[out.get_fdata() > 0.5] = 1 95 | out.get_fdata()[out.get_fdata() < 0.5] = 0 96 | return out 97 | 98 | 99 | def expand_mask(mask, custom_mask=None): 100 | """expand a mask with multiple integers into separate binary masks 101 | 102 | Args: 103 | mask: nibabel or Brain_Data instance 104 | custom_mask: nibabel instance or string to file path; optional 105 | 106 | Returns: 107 | out: Brain_Data instance of multiple binary masks 108 | 109 | """ 110 | 111 | from nltools.data import Brain_Data 112 | 113 | if isinstance(mask, nib.Nifti1Image): 114 | mask = Brain_Data(mask, mask=custom_mask) 115 | if not isinstance(mask, Brain_Data): 116 | raise ValueError("Make sure mask is a nibabel or Brain_Data instance.") 117 | mask.data = np.round(mask.data).astype(int) 118 | tmp = [] 119 | for i in np.nonzero(np.unique(mask.data))[0]: 120 | tmp.append((mask.data == i) * 1) 121 | out = mask.empty() 122 | out.data = np.array(tmp) 123 | return out 124 | 125 | 126 | def collapse_mask(mask, auto_label=True, custom_mask=None): 127 | """collapse separate masks into one mask with multiple integers 128 | overlapping areas are ignored 129 | 130 | Args: 131 | mask: nibabel or Brain_Data instance 132 | custom_mask: nibabel instance or string to file path; optional 133 | 134 | Returns: 135 | out: Brain_Data instance of a mask with different integers indicating 136 | different masks 137 | 138 | """ 139 | 140 | from nltools.data import Brain_Data 141 | 142 | if not isinstance(mask, Brain_Data): 143 | if isinstance(mask, nib.Nifti1Image): 144 | mask = Brain_Data(mask, mask=custom_mask) 145 | else: 146 | raise ValueError("Make sure mask is a nibabel or Brain_Data " "instance.") 147 | 148 | if len(mask.shape()) > 1: 149 | if len(mask) > 1: 150 | out = mask.empty() 151 | 152 | # Create list of masks and find any overlaps 153 | m_list = [] 154 | for x in range(len(mask)): 155 | m_list.append(mask[x].to_nifti()) 156 | intersect = intersect_masks(m_list, threshold=1, connected=False) 157 | intersect = Brain_Data( 158 | nib.Nifti1Image(np.abs(intersect.get_fdata() - 1), intersect.affine), 159 | mask=custom_mask, 160 | ) 161 | 162 | merge = [] 163 | if auto_label: 164 | # Combine all masks into sequential order 165 | # ignoring any areas of overlap 166 | for i in range(len(m_list)): 167 | merge.append( 168 | np.multiply( 169 | Brain_Data(m_list[i], mask=custom_mask).data, intersect.data 170 | ) 171 | * (i + 1) 172 | ) 173 | out.data = np.sum(np.array(merge).T, 1).astype(int) 174 | else: 175 | # Collapse masks using value as label 176 | for i in range(len(m_list)): 177 | merge.append( 178 | np.multiply( 179 | Brain_Data(m_list[i], mask=custom_mask).data, intersect.data 180 | ) 181 | ) 182 | out.data = np.sum(np.array(merge).T, 1) 183 | return out 184 | else: 185 | warnings.warn("Doesn't need to be collapased") 186 | 187 | 188 | def roi_to_brain(data, mask_x): 189 | """This function will create convert an expanded binary mask of ROIs 190 | (see expand_mask) based on a vector of of values. The dataframe of values 191 | must correspond to ROI numbers. 192 | 193 | This is useful for populating a parcellation scheme by a vector of Values 194 | 195 | Args: 196 | data: Pandas series, dataframe, list, np.array of ROI by observation 197 | mask_x: an expanded binary mask 198 | 199 | Returns: 200 | out: (Brain_Data) Brain_Data instance where each ROI is now populated 201 | with a value 202 | """ 203 | from nltools.data import Brain_Data 204 | 205 | if not isinstance(data, (pd.Series, pd.DataFrame)): 206 | if isinstance(data, list): 207 | data = pd.Series(data) 208 | elif isinstance(data, np.ndarray): 209 | if len(data.shape) == 1: 210 | data = pd.Series(data) 211 | elif len(data.shape) == 2: 212 | data = pd.DataFrame(data) 213 | if data.shape[0] != len(mask_x): 214 | if data.shape[1] == len(mask_x): 215 | data = data.T 216 | else: 217 | raise ValueError( 218 | "Data must have the same number of rows as rois in mask" 219 | ) 220 | else: 221 | raise NotImplementedError 222 | 223 | else: 224 | raise ValueError("Data must be a pandas series or data frame.") 225 | 226 | if len(mask_x) != data.shape[0]: 227 | raise ValueError("Data must have the same number of rows as mask has ROIs.") 228 | 229 | if isinstance(data, pd.Series): 230 | out = mask_x[0].copy() 231 | out.data = np.zeros(out.data.shape) 232 | for roi in range(len(mask_x)): 233 | out.data[np.where(mask_x.data[roi, :])] = data[roi] 234 | return out 235 | else: 236 | out = mask_x.copy() 237 | out.data = np.ones((data.shape[1], out.data.shape[1])) 238 | for roi in range(len(mask_x)): 239 | roi_data = np.reshape(data.iloc[roi, :].values, (-1, 1)) 240 | out.data[:, mask_x[roi].data == 1] = np.repeat( 241 | roi_data.T, np.sum(mask_x[roi].data == 1), axis=0 242 | ).T 243 | return out 244 | -------------------------------------------------------------------------------- /nltools/prefs.py: -------------------------------------------------------------------------------- 1 | import os 2 | from nltools.utils import get_resource_path 3 | 4 | __all__ = ["MNI_Template", "resolve_mni_path"] 5 | 6 | 7 | class MNI_Template_Factory(dict): 8 | """Class to build the default MNI_Template dictionary. This should never be used 9 | directly, instead just `from nltools.prefs import MNI_Template` and update that 10 | object's attributes to change MNI templates.""" 11 | 12 | def __init__( 13 | self, 14 | resolution="2mm", 15 | mask_type="with_ventricles", 16 | mask=os.path.join(get_resource_path(), "MNI152_T1_2mm_brain_mask.nii.gz"), 17 | plot=os.path.join(get_resource_path(), "MNI152_T1_2mm.nii.gz"), 18 | brain=os.path.join(get_resource_path(), "MNI152_T1_2mm_brain.nii.gz"), 19 | ): 20 | self._resolution = resolution 21 | self._mask_type = mask_type 22 | self._mask = mask 23 | self._plot = plot 24 | self._brain = brain 25 | 26 | self.update( 27 | { 28 | "resolution": self.resolution, 29 | "mask_type": self.mask_type, 30 | "mask": self.mask, 31 | "plot": self.plot, 32 | "brain": self.brain, 33 | } 34 | ) 35 | 36 | @property 37 | def resolution(self): 38 | return self._resolution 39 | 40 | @resolution.setter 41 | def resolution(self, resolution): 42 | if isinstance(resolution, (int, float)): 43 | resolution = f"{int(resolution)}mm" 44 | if resolution not in ["2mm", "3mm"]: 45 | raise NotImplementedError( 46 | "Only 2mm and 3mm resolutions are currently supported" 47 | ) 48 | self._resolution = resolution 49 | self.update({"resolution": self._resolution}) 50 | 51 | @property 52 | def mask_type(self): 53 | return self._mask_type 54 | 55 | @mask_type.setter 56 | def mask_type(self, mask_type): 57 | if mask_type not in ["with_ventricles", "no_ventricles"]: 58 | raise NotImplementedError( 59 | "Only 'with_ventricles' and 'no_ventricles' mask_types are currently supported" 60 | ) 61 | self._mask_type = mask_type 62 | self.update({"mask_type": self._mask_type}) 63 | 64 | @property 65 | def mask(self): 66 | return self._mask 67 | 68 | @mask.setter 69 | def mask(self, mask): 70 | self._mask = mask 71 | self.update({"mask": self._mask}) 72 | 73 | @property 74 | def plot(self): 75 | return self._plot 76 | 77 | @plot.setter 78 | def plot(self, plot): 79 | self._plot = plot 80 | self.update({"plot": self._plot}) 81 | 82 | @property 83 | def brain(self): 84 | return self._brain 85 | 86 | @brain.setter 87 | def brain(self, brain): 88 | self._brain = brain 89 | self.update({"brain": self._brain}) 90 | 91 | 92 | # NOTE: We export this from the module and expect users to interact with it instead of 93 | # the class constructor above 94 | MNI_Template = MNI_Template_Factory() 95 | 96 | 97 | def resolve_mni_path(MNI_Template): 98 | """Helper function to resolve MNI path based on MNI_Template prefs setting.""" 99 | 100 | res = MNI_Template["resolution"] 101 | m = MNI_Template["mask_type"] 102 | if not isinstance(res, str): 103 | raise ValueError("resolution must be provided as a string!") 104 | if not isinstance(m, str): 105 | raise ValueError("mask_type must be provided as a string!") 106 | 107 | if res == "3mm": 108 | if m == "with_ventricles": 109 | MNI_Template["mask"] = os.path.join( 110 | get_resource_path(), "MNI152_T1_3mm_brain_mask.nii.gz" 111 | ) 112 | elif m == "no_ventricles": 113 | MNI_Template["mask"] = os.path.join( 114 | get_resource_path(), "MNI152_T1_3mm_brain_mask_no_ventricles.nii.gz" 115 | ) 116 | else: 117 | raise ValueError( 118 | "Available mask_types are 'with_ventricles' or 'no_ventricles'" 119 | ) 120 | 121 | MNI_Template["plot"] = os.path.join(get_resource_path(), "MNI152_T1_3mm.nii.gz") 122 | 123 | MNI_Template["brain"] = os.path.join( 124 | get_resource_path(), "MNI152_T1_3mm_brain.nii.gz" 125 | ) 126 | 127 | elif res == "2mm": 128 | if m == "with_ventricles": 129 | MNI_Template["mask"] = os.path.join( 130 | get_resource_path(), "MNI152_T1_2mm_brain_mask.nii.gz" 131 | ) 132 | elif m == "no_ventricles": 133 | MNI_Template["mask"] = os.path.join( 134 | get_resource_path(), "MNI152_T1_2mm_brain_mask_no_ventricles.nii.gz" 135 | ) 136 | else: 137 | raise ValueError( 138 | "Available mask_types are 'with_ventricles' or 'no_ventricles'" 139 | ) 140 | 141 | MNI_Template["plot"] = os.path.join(get_resource_path(), "MNI152_T1_2mm.nii.gz") 142 | 143 | MNI_Template["brain"] = os.path.join( 144 | get_resource_path(), "MNI152_T1_2mm_brain.nii.gz" 145 | ) 146 | else: 147 | raise ValueError("Available templates are '2mm' or '3mm'") 148 | return MNI_Template 149 | -------------------------------------------------------------------------------- /nltools/resources/MNI152_T1_2mm.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosanlab/nltools/41219c5bb5adf5448b515a7bffb19eccbf9f35d7/nltools/resources/MNI152_T1_2mm.nii.gz -------------------------------------------------------------------------------- /nltools/resources/MNI152_T1_2mm_brain.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosanlab/nltools/41219c5bb5adf5448b515a7bffb19eccbf9f35d7/nltools/resources/MNI152_T1_2mm_brain.nii.gz -------------------------------------------------------------------------------- /nltools/resources/MNI152_T1_2mm_brain_mask.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosanlab/nltools/41219c5bb5adf5448b515a7bffb19eccbf9f35d7/nltools/resources/MNI152_T1_2mm_brain_mask.nii.gz -------------------------------------------------------------------------------- /nltools/resources/MNI152_T1_2mm_brain_mask_no_ventricles.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosanlab/nltools/41219c5bb5adf5448b515a7bffb19eccbf9f35d7/nltools/resources/MNI152_T1_2mm_brain_mask_no_ventricles.nii.gz -------------------------------------------------------------------------------- /nltools/resources/MNI152_T1_3mm.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosanlab/nltools/41219c5bb5adf5448b515a7bffb19eccbf9f35d7/nltools/resources/MNI152_T1_3mm.nii.gz -------------------------------------------------------------------------------- /nltools/resources/MNI152_T1_3mm_brain.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosanlab/nltools/41219c5bb5adf5448b515a7bffb19eccbf9f35d7/nltools/resources/MNI152_T1_3mm_brain.nii.gz -------------------------------------------------------------------------------- /nltools/resources/MNI152_T1_3mm_brain_mask.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosanlab/nltools/41219c5bb5adf5448b515a7bffb19eccbf9f35d7/nltools/resources/MNI152_T1_3mm_brain_mask.nii.gz -------------------------------------------------------------------------------- /nltools/resources/MNI152_T1_3mm_brain_mask_no_ventricles.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosanlab/nltools/41219c5bb5adf5448b515a7bffb19eccbf9f35d7/nltools/resources/MNI152_T1_3mm_brain_mask_no_ventricles.nii.gz -------------------------------------------------------------------------------- /nltools/resources/gm_mask_2mm.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosanlab/nltools/41219c5bb5adf5448b515a7bffb19eccbf9f35d7/nltools/resources/gm_mask_2mm.nii.gz -------------------------------------------------------------------------------- /nltools/resources/gm_mask_3mm.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosanlab/nltools/41219c5bb5adf5448b515a7bffb19eccbf9f35d7/nltools/resources/gm_mask_3mm.nii.gz -------------------------------------------------------------------------------- /nltools/resources/onsets_example.txt: -------------------------------------------------------------------------------- 1 | Onset,Stim 2 | 10.1605967,CoachTaylor 3 | 18.190957161,LylaGarrity 4 | 26.221317192,JulieTaylor 5 | 33.252048014,LylaGarrity 6 | 40.282787781,LandryClarke 7 | 51.311997544,BuddyGarrity 8 | 57.343152722,TamiTaylor 9 | 63.37425517,BuddyGarrity 10 | 69.405359036,SmashWilliams 11 | 76.436088332,SmashWilliams 12 | 83.466820048,GrandmaSaracen 13 | 91.497193631,BillyRiggins 14 | 101.526809594,MattSaracen 15 | 108.557544453,TamiTaylor 16 | 114.588578367,TamiTaylor 17 | 120.61974369,GrandmaSaracen 18 | 126.650846005,BillyRiggins 19 | 132.681946075,LylaGarrity 20 | 142.71156909,LandryClarke 21 | 156.739712911,BuddyGarrity 22 | 162.770814649,JasonStreet 23 | 173.800055801,TimRiggins 24 | 180.830786482,CoachTaylor 25 | 186.86185978,JasonStreet 26 | 192.892958472,CoachTaylor 27 | 202.922615368,JasonStreet 28 | 208.95371632,GrandmaSaracen 29 | 220.982600503,MattSaracen 30 | 234.011058731,BillyRiggins 31 | 240.042214664,JasonStreet 32 | 249.072207216,TimRiggins 33 | 256.102931267,BillyRiggins 34 | 263.133663523,JulieTaylor 35 | 272.163655973,JulieTaylor 36 | 281.193644014,JasonStreet 37 | 289.223956762,TyraCollette 38 | 297.254368242,GrandmaSaracen 39 | 304.285099479,MattSaracen 40 | 313.315087766,BuddyGarrity 41 | -------------------------------------------------------------------------------- /nltools/resources/onsets_example_with_dur.txt: -------------------------------------------------------------------------------- 1 | Stim,Onset,Duration 2 | inv_onset,3.0,4 3 | mult_onset,11.0,4 4 | pred_onset,17.0,8 5 | ret_onset,30.0,6 6 | inv_onset,39.0,4 7 | mult_onset,45.0,4 8 | pred_onset,52.0,8 9 | ret_onset,63.0,6 10 | inv_onset,72.0,4 11 | mult_onset,78.0,4 12 | pred_onset,85.0,8 13 | ret_onset,95.0,6 14 | inv_onset,104.0,4 15 | mult_onset,110.0,4 16 | pred_onset,117.0,8 17 | ret_onset,127.0,6 18 | inv_onset,136.0,4 19 | mult_onset,148.0,4 20 | pred_onset,156.0,8 21 | ret_onset,166.0,6 22 | inv_onset,175.0,4 23 | mult_onset,182.0,4 24 | pred_onset,191.0,8 25 | ret_onset,204.0,6 26 | inv_onset,213.0,4 27 | mult_onset,220.0,4 28 | pred_onset,226.0,8 29 | ret_onset,236.0,6 30 | inv_onset,247.0,4 31 | mult_onset,253.0,4 32 | pred_onset,260.0,8 33 | ret_onset,271.0,6 34 | inv_onset,281.0,4 35 | mult_onset,287.0,4 36 | pred_onset,293.0,8 37 | ret_onset,304.0,6 38 | inv_onset,312.0,4 39 | mult_onset,318.0,4 40 | pred_onset,324.0,8 41 | ret_onset,335.0,6 42 | inv_onset,344.0,4 43 | mult_onset,352.0,4 44 | pred_onset,358.0,8 45 | ret_onset,370.0,6 46 | inv_onset,378.0,4 47 | mult_onset,384.0,4 48 | pred_onset,392.0,8 49 | ret_onset,403.0,6 50 | inv_onset,413.0,4 51 | mult_onset,420.0,4 52 | pred_onset,427.0,8 53 | ret_onset,440.0,6 54 | inv_onset,449.0,4 55 | mult_onset,456.0,4 56 | pred_onset,463.0,8 57 | ret_onset,475.0,6 58 | inv_onset,486.0,4 59 | mult_onset,492.0,4 60 | pred_onset,498.0,8 61 | ret_onset,509.0,6 62 | inv_onset,518.0,4 63 | mult_onset,526.0,4 64 | pred_onset,532.0,8 65 | ret_onset,543.0,6 66 | inv_onset,555.0,4 67 | mult_onset,561.0,4 68 | pred_onset,568.0,8 69 | ret_onset,578.0,6 70 | inv_onset,588.0,4 71 | mult_onset,595.0,4 72 | pred_onset,603.0,8 73 | ret_onset,615.0,6 74 | inv_onset,624.0,4 75 | mult_onset,634.0,4 76 | pred_onset,644.0,8 77 | ret_onset,654.0,6 78 | inv_onset,664.0,4 79 | mult_onset,670.0,4 80 | pred_onset,679.0,8 81 | ret_onset,690.0,6 82 | inv_onset,699.0,4 83 | mult_onset,706.0,4 84 | pred_onset,713.0,8 85 | ret_onset,723.0,6 86 | inv_onset,732.0,4 87 | mult_onset,742.0,4 88 | pred_onset,751.0,8 89 | ret_onset,761.0,6 90 | inv_onset,769.0,4 91 | mult_onset,776.0,4 92 | pred_onset,785.0,8 93 | ret_onset,795.0,6 94 | inv_onset,803.0,4 95 | mult_onset,810.0,4 96 | pred_onset,817.0,8 97 | ret_onset,828.0,6 98 | inv_onset,836.0,4 99 | mult_onset,843.0,4 100 | pred_onset,849.0,8 101 | ret_onset,859.0,6 102 | inv_onset,869.0,4 103 | mult_onset,875.0,4 104 | pred_onset,881.0,8 105 | ret_onset,891.0,6 106 | inv_onset,902.0,4 107 | mult_onset,910.0,4 108 | pred_onset,916.0,8 109 | ret_onset,927.0,6 110 | -------------------------------------------------------------------------------- /nltools/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | import pandas as pd 4 | from sklearn.metrics import pairwise_distances 5 | from nltools.simulator import Simulator 6 | from nltools.data import Brain_Data, Adjacency, Groupby, Design_Matrix 7 | from nltools.mask import create_sphere 8 | import os 9 | 10 | 11 | @pytest.fixture(scope="module", params=["2mm"]) 12 | def sim_brain_data(): 13 | np.random.seed(0) 14 | # MNI_Template["resolution"] = request.params 15 | sim = Simulator() 16 | r = 10 17 | sigma = 1 18 | y = [0, 1] 19 | n_reps = 3 20 | dat = sim.create_data(y, sigma, reps=n_reps) 21 | dat.X = pd.DataFrame( 22 | {"Intercept": np.ones(len(dat.Y)), "X1": np.array(dat.Y).flatten()}, index=None 23 | ) 24 | return dat 25 | 26 | 27 | @pytest.fixture(scope="module") 28 | def sim_design_matrix(): 29 | np.random.seed(0) 30 | # Design matrices are specified in terms of sampling frequency 31 | TR = 2.0 32 | sampling_freq = 1.0 / TR 33 | return Design_Matrix( 34 | np.random.randint(2, size=(500, 4)), 35 | columns=["face_A", "face_B", "house_A", "house_B"], 36 | sampling_freq=sampling_freq, 37 | ) 38 | 39 | 40 | @pytest.fixture(scope="module") 41 | def sim_adjacency_single(): 42 | np.random.seed(0) 43 | sim = np.random.multivariate_normal( 44 | [0, 0, 0, 0], 45 | [ 46 | [1, 0.8, 0.1, 0.4], 47 | [0.8, 1, 0.6, 0.1], 48 | [0.1, 0.6, 1, 0.3], 49 | [0.4, 0.1, 0.3, 1], 50 | ], 51 | 100, 52 | ) 53 | data = pairwise_distances(sim.T, metric="correlation") 54 | labels = ["v_%s" % (x + 1) for x in range(sim.shape[1])] 55 | return Adjacency(data, labels=labels) 56 | 57 | 58 | @pytest.fixture(scope="module") 59 | def sim_adjacency_multiple(): 60 | np.random.seed(0) 61 | n = 10 62 | sim = np.random.multivariate_normal( 63 | [0, 0, 0, 0], 64 | [ 65 | [1, 0.8, 0.1, 0.4], 66 | [0.8, 1, 0.6, 0.1], 67 | [0.1, 0.6, 1, 0.3], 68 | [0.4, 0.1, 0.3, 1], 69 | ], 70 | 100, 71 | ) 72 | data = pairwise_distances(sim.T, metric="correlation") 73 | dat_all = [] 74 | for t in range(n): 75 | tmp = data 76 | dat_all.append(tmp) 77 | labels = ["v_%s" % (x + 1) for x in range(sim.shape[1])] 78 | return Adjacency(dat_all, labels=labels) 79 | 80 | 81 | @pytest.fixture(scope="module") 82 | def sim_adjacency_directed(): 83 | sim_directed = np.array( 84 | [ 85 | [1, 0.5, 0.3, 0.4], 86 | [0.8, 1, 0.2, 0.1], 87 | [0.7, 0.6, 1, 0.5], 88 | [0.85, 0.4, 0.3, 1], 89 | ] 90 | ) 91 | labels = ["v_%s" % (x + 1) for x in range(sim_directed.shape[1])] 92 | return Adjacency(sim_directed, matrix_type="directed", labels=labels) 93 | 94 | 95 | @pytest.fixture(scope="module") 96 | def sim_groupby(sim_brain_data): 97 | r = 10 98 | s1 = create_sphere([12, 10, -8], radius=r) 99 | s2 = create_sphere([22, -2, -22], radius=r) 100 | mask = Brain_Data([s1, s2]) 101 | return Groupby(sim_brain_data, mask) 102 | 103 | 104 | @pytest.fixture(scope="module") 105 | def old_h5_brain(request): 106 | test_dir = os.path.dirname(request.module.__file__) 107 | return os.path.join(test_dir, "old_brain.h5") 108 | 109 | 110 | @pytest.fixture(scope="module") 111 | def new_h5_brain(request): 112 | test_dir = os.path.dirname(request.module.__file__) 113 | return os.path.join(test_dir, "new_brain.h5") 114 | 115 | 116 | @pytest.fixture(scope="module") 117 | def old_h5_adj_single(request): 118 | test_dir = os.path.dirname(request.module.__file__) 119 | return os.path.join(test_dir, "old_single.h5") 120 | 121 | 122 | @pytest.fixture(scope="module") 123 | def new_h5_adj_single(request): 124 | test_dir = os.path.dirname(request.module.__file__) 125 | return os.path.join(test_dir, "new_single.h5") 126 | 127 | 128 | @pytest.fixture(scope="module") 129 | def old_h5_adj_double(request): 130 | test_dir = os.path.dirname(request.module.__file__) 131 | return os.path.join(test_dir, "old_double.h5") 132 | 133 | 134 | @pytest.fixture(scope="module") 135 | def new_h5_adj_double(request): 136 | test_dir = os.path.dirname(request.module.__file__) 137 | return os.path.join(test_dir, "new_double.h5") 138 | -------------------------------------------------------------------------------- /nltools/tests/matplotlibrc: -------------------------------------------------------------------------------- 1 | backend : Agg 2 | -------------------------------------------------------------------------------- /nltools/tests/new_brain.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosanlab/nltools/41219c5bb5adf5448b515a7bffb19eccbf9f35d7/nltools/tests/new_brain.h5 -------------------------------------------------------------------------------- /nltools/tests/new_double.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosanlab/nltools/41219c5bb5adf5448b515a7bffb19eccbf9f35d7/nltools/tests/new_double.h5 -------------------------------------------------------------------------------- /nltools/tests/new_single.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosanlab/nltools/41219c5bb5adf5448b515a7bffb19eccbf9f35d7/nltools/tests/new_single.h5 -------------------------------------------------------------------------------- /nltools/tests/old_brain.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosanlab/nltools/41219c5bb5adf5448b515a7bffb19eccbf9f35d7/nltools/tests/old_brain.h5 -------------------------------------------------------------------------------- /nltools/tests/old_double.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosanlab/nltools/41219c5bb5adf5448b515a7bffb19eccbf9f35d7/nltools/tests/old_double.h5 -------------------------------------------------------------------------------- /nltools/tests/old_single.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosanlab/nltools/41219c5bb5adf5448b515a7bffb19eccbf9f35d7/nltools/tests/old_single.h5 -------------------------------------------------------------------------------- /nltools/tests/test_analysis.py: -------------------------------------------------------------------------------- 1 | from nltools.simulator import Simulator 2 | from nltools.analysis import Roc 3 | 4 | 5 | def test_roc(tmpdir): 6 | sim = Simulator() 7 | sigma = 0.1 8 | y = [0, 1] 9 | n_reps = 10 10 | # output_dir = str(tmpdir) 11 | dat = sim.create_data(y, sigma, reps=n_reps, output_dir=None) 12 | # dat = Brain_Data(data=sim.data, Y=sim.y) 13 | 14 | algorithm = "svm" 15 | # output_dir = str(tmpdir) 16 | # cv = {'type': 'kfolds', 'n_folds': 5, 'subject_id': sim.rep_id} 17 | extra = {"kernel": "linear"} 18 | 19 | output = dat.predict(algorithm=algorithm, plot=False, **extra) 20 | 21 | # Single-Interval 22 | roc = Roc(input_values=output["yfit_all"], binary_outcome=output["Y"] == 1) 23 | roc.calculate() 24 | roc.summary() 25 | assert roc.accuracy == 1 26 | 27 | # Forced Choice 28 | binary_outcome = output["Y"] == 1 29 | forced_choice = list(range(int(len(binary_outcome) / 2))) + list( 30 | range(int(len(binary_outcome) / 2)) 31 | ) 32 | forced_choice = forced_choice.sort() 33 | roc_fc = Roc( 34 | input_values=output["yfit_all"], 35 | binary_outcome=binary_outcome, 36 | forced_choice=forced_choice, 37 | ) 38 | roc_fc.calculate() 39 | assert roc_fc.accuracy == 1 40 | assert roc_fc.accuracy == roc_fc.auc == roc_fc.sensitivity == roc_fc.specificity 41 | -------------------------------------------------------------------------------- /nltools/tests/test_cross_validation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from nltools.cross_validation import KFoldStratified 4 | 5 | 6 | def check_valid_split(train, test, n_samples=None): 7 | # Use python sets to get more informative assertion failure messages 8 | train, test = set(train), set(test) 9 | 10 | # Train and test split should not overlap 11 | assert train.intersection(test) == set() 12 | 13 | if n_samples is not None: 14 | # Check that the union of train an test split cover all the indices 15 | assert train.union(test) == set(range(n_samples)) 16 | 17 | 18 | def check_cv_coverage(cv, X, y, groups, expected_n_splits=None): 19 | n_samples = X.shape[0] 20 | # Check that a all the samples appear at least once in a test fold 21 | if expected_n_splits is not None: 22 | assert cv.get_n_splits(X, y, groups) == expected_n_splits 23 | else: 24 | expected_n_splits = cv.get_n_splits(X, y, groups) 25 | 26 | collected_test_samples = set() 27 | iterations = 0 28 | for train, test in cv.split(X, y, groups): 29 | check_valid_split(train, test, n_samples=n_samples) 30 | iterations += 1 31 | collected_test_samples.update(test) 32 | 33 | # Check that the accumulated test samples cover the whole dataset 34 | assert iterations == expected_n_splits 35 | if n_samples is not None: 36 | assert collected_test_samples == set(range(n_samples)) 37 | 38 | 39 | def test_stratified_kfold_ratios(): 40 | y = pd.DataFrame(np.random.randn(1000)) * 20 + 50 41 | n_folds = 5 42 | cv = KFoldStratified(n_splits=n_folds) 43 | for train, test in cv.split(np.zeros(len(y)), y): 44 | assert (y.iloc[train].mean()[0] >= 47) & (y.iloc[train].mean()[0] <= 53) 45 | 46 | 47 | def test_kfoldstratified(): 48 | y = pd.DataFrame(np.random.randn(50)) * 20 + 50 49 | n_folds = 5 50 | cv = KFoldStratified(n_splits=n_folds) 51 | check_cv_coverage( 52 | cv, X=np.zeros(len(y)), y=y, groups=None, expected_n_splits=n_folds 53 | ) 54 | 55 | y = pd.DataFrame(np.random.randn(51)) * 20 + 50 56 | n_folds = 5 57 | cv = KFoldStratified(n_splits=n_folds) 58 | check_cv_coverage( 59 | cv, X=np.zeros(len(y)), y=y, groups=None, expected_n_splits=n_folds 60 | ) 61 | -------------------------------------------------------------------------------- /nltools/tests/test_design_matrix.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from nltools.data import Design_Matrix 4 | from nltools.external.hrf import glover_hrf 5 | import pytest 6 | 7 | 8 | def test_add_poly(sim_design_matrix): 9 | matp = sim_design_matrix.add_poly(2) 10 | assert matp.shape[1] == 7 11 | assert sim_design_matrix.add_poly(2, include_lower=False).shape[1] == 5 12 | 13 | 14 | def test_add_dct_basis(sim_design_matrix): 15 | matpd = sim_design_matrix.add_dct_basis() 16 | assert matpd.shape[1] == 15 17 | 18 | 19 | def test_vif(sim_design_matrix): 20 | matpd = sim_design_matrix.add_poly(2).add_dct_basis() 21 | assert all(matpd.vif() < 2.0) 22 | assert not all(matpd.vif(exclude_polys=False) < 2.0) 23 | matc = matpd.clean() 24 | assert matc.shape[1] == 16 25 | 26 | 27 | def test_convolve(sim_design_matrix): 28 | TR = 2.0 29 | assert sim_design_matrix.convolve().shape == sim_design_matrix.shape 30 | hrf = glover_hrf(TR, oversampling=1.0) 31 | assert ( 32 | sim_design_matrix.convolve(conv_func=np.column_stack([hrf, hrf])).shape[1] 33 | == sim_design_matrix.shape[1] + 4 34 | ) 35 | 36 | 37 | def test_zscore(sim_design_matrix): 38 | matz = sim_design_matrix.zscore(columns=["face_A", "face_B"]) 39 | assert ( 40 | (matz[["house_A", "house_B"]] == sim_design_matrix[["house_A", "house_B"]]) 41 | .all() 42 | .all() 43 | ) 44 | 45 | 46 | def test_replace(sim_design_matrix): 47 | assert ( 48 | sim_design_matrix.replace_data(np.zeros((500, 4))).shape 49 | == sim_design_matrix.shape 50 | ) 51 | 52 | 53 | def test_upsample(sim_design_matrix): 54 | newTR = 1.0 55 | target = 1.0 / newTR 56 | assert ( 57 | sim_design_matrix.upsample(target).shape[0] 58 | == sim_design_matrix.shape[0] * 2 - target * 2 59 | ) 60 | 61 | 62 | def test_downsample(sim_design_matrix): 63 | newTR = 4.0 64 | target = 1.0 / newTR 65 | assert ( 66 | sim_design_matrix.downsample(target).shape[0] == sim_design_matrix.shape[0] / 2 67 | ) 68 | 69 | 70 | def test_append(sim_design_matrix): 71 | mats = sim_design_matrix.append(sim_design_matrix) 72 | assert mats.shape[0] == sim_design_matrix.shape[0] * 2 73 | # Keep polys separate by default 74 | 75 | assert (mats.shape[1] - 4) == (sim_design_matrix.shape[1] - 4) * 2 76 | # Otherwise stack them 77 | mats = sim_design_matrix.append(sim_design_matrix, keep_separate=False) 78 | assert mats.shape[1] == sim_design_matrix.shape[1] 79 | assert mats.shape[0] == sim_design_matrix.shape[0] * 2 80 | 81 | # Keep a single stimulus column separate 82 | assert ( 83 | sim_design_matrix.append(sim_design_matrix, unique_cols=["face_A"]).shape[1] 84 | == 5 85 | ) 86 | 87 | # Keep a common stimulus class separate 88 | assert ( 89 | sim_design_matrix.append(sim_design_matrix, unique_cols=["face*"]).shape[1] == 6 90 | ) 91 | # Keep a common stimulus class and a different single stim separate 92 | assert ( 93 | sim_design_matrix.append( 94 | sim_design_matrix, unique_cols=["face*", "house_A"] 95 | ).shape[1] 96 | == 7 97 | ) 98 | # Keep multiple stimulus class separate 99 | assert ( 100 | sim_design_matrix.append( 101 | sim_design_matrix, unique_cols=["face*", "house*"] 102 | ).shape[1] 103 | == 8 104 | ) 105 | 106 | # Growing a multi-run design matrix; keeping things separate 107 | num_runs = 4 108 | all_runs = Design_Matrix(sampling_freq=0.5) 109 | for i in range(num_runs): 110 | run = Design_Matrix( 111 | np.array( 112 | [ 113 | [1, 0, 0, 0], 114 | [1, 0, 0, 0], 115 | [0, 0, 0, 0], 116 | [0, 1, 0, 0], 117 | [0, 1, 0, 0], 118 | [0, 0, 0, 0], 119 | [0, 0, 1, 0], 120 | [0, 0, 1, 0], 121 | [0, 0, 0, 0], 122 | [0, 0, 0, 1], 123 | [0, 0, 0, 1], 124 | ] 125 | ), 126 | sampling_freq=0.5, 127 | columns=["stim_A", "stim_B", "cond_C", "cond_D"], 128 | ) 129 | run = run.add_poly(2) 130 | all_runs = all_runs.append(run, unique_cols=["stim*", "cond*"]) 131 | assert all_runs.shape == (44, 28) 132 | 133 | 134 | def test_clean(sim_design_matrix): 135 | # Drop correlated column 136 | corr_cols = sim_design_matrix.assign(new_col=lambda df: df.iloc[:, 0]) 137 | out = corr_cols.clean(verbose=True) 138 | assert out.shape[1] < corr_cols.shape[1] 139 | 140 | # Test for bug #413 about args combinations 141 | out = corr_cols.clean(fill_na=None, exclude_polys=True, verbose=True) 142 | assert out.shape[1] < corr_cols.shape[1] 143 | 144 | # Raise an error if try to clean with an input matrix that has duplicate column names 145 | dup_cols = pd.concat([sim_design_matrix, sim_design_matrix], axis=1) 146 | with pytest.raises(ValueError): 147 | _ = dup_cols.clean() 148 | -------------------------------------------------------------------------------- /nltools/tests/test_file_reader.py: -------------------------------------------------------------------------------- 1 | from nltools.data import Design_Matrix 2 | from nltools.file_reader import onsets_to_dm 3 | from nltools.utils import get_resource_path 4 | import numpy as np 5 | import pandas as pd 6 | import os 7 | 8 | 9 | def test_onsets_to_dm(): 10 | fpath = os.path.join(get_resource_path(), "onsets_example.txt") 11 | data = pd.read_csv(os.path.join(get_resource_path(), "onsets_example.txt")) 12 | sampling_freq = 0.5 13 | run_length = 1364 14 | Duration = 10 15 | TR = 1 / sampling_freq 16 | 17 | # Two-column 18 | # Test loading from a file 19 | dm = onsets_to_dm(fpath, sampling_freq, run_length) 20 | assert isinstance(dm, Design_Matrix) 21 | 22 | # Check it has run_length rows and nStim columns 23 | assert dm.shape == (run_length, data.Stim.nunique()) 24 | 25 | # Get the unique number of presentations of each Stim from the original file 26 | stim_counts = data.Stim.value_counts(sort=False)[dm.columns] 27 | 28 | # Check there are only as many onsets as occurences of each Stim 29 | np.allclose(stim_counts.values, dm.sum().values) 30 | 31 | # Three-column with loading from dataframe 32 | data["Duration"] = Duration 33 | dm = onsets_to_dm(data, sampling_freq, run_length) 34 | 35 | # Check it has run_length rows and nStim columns 36 | assert dm.shape == (run_length, data.Stim.nunique()) 37 | 38 | # Because timing varies in seconds and isn't TR-locked each stimulus should last at Duration/TR number of TRs and at most Duration/TR + 1 TRs 39 | # Check that the total number of TRs for each stimulus >= 1 + (Duration/TR) and <= 1 + (Duration/TR + 1) 40 | onsets = dm.sum().values 41 | durations = data.groupby("Stim").Duration.mean().values 42 | for o, c, d in zip(onsets, stim_counts, durations): 43 | assert c * (d / TR) <= o <= c * ((d / TR) + 1) 44 | 45 | # Multiple onsets 46 | dm = onsets_to_dm([data, data], sampling_freq, run_length) 47 | 48 | # Check it has run_length rows and nStim columns 49 | assert dm.shape == (run_length * 2, data.Stim.nunique()) 50 | 51 | # Multiple onsets with polynomials auto-added 52 | dm = onsets_to_dm([data, data], sampling_freq, run_length, add_poly=2) 53 | assert dm.shape == (run_length * 2, data.Stim.nunique() + (3 * 2)) 54 | 55 | dm = onsets_to_dm( 56 | [data, data], sampling_freq, run_length, add_poly=2, keep_separate=False 57 | ) 58 | assert dm.shape == (run_length * 2, data.Stim.nunique() + 3) 59 | 60 | # Three-column from file with variable durations 61 | data = pd.read_csv(os.path.join(get_resource_path(), "onsets_example_with_dur.txt")) 62 | run_length = 472 63 | dm = onsets_to_dm(data, sampling_freq, run_length) 64 | 65 | assert dm.shape == (run_length, data.Stim.nunique()) 66 | 67 | onsets = dm.sum().values 68 | stim_counts = data.Stim.value_counts().values 69 | durations = data.groupby("Stim").Duration.mean().values 70 | for o, c, d in zip(onsets, stim_counts, durations): 71 | assert c * (d / TR) <= o <= c * ((d / TR) + 1) 72 | -------------------------------------------------------------------------------- /nltools/tests/test_groupby.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from nltools.data import Brain_Data 3 | 4 | 5 | def test_length(sim_groupby): 6 | assert len(sim_groupby) == len(sim_groupby.mask) 7 | 8 | 9 | def test_index(sim_groupby): 10 | assert isinstance(sim_groupby[1], Brain_Data) 11 | 12 | 13 | def test_apply(sim_groupby): 14 | mn = sim_groupby.apply("mean") 15 | assert len(sim_groupby) == len(mn) 16 | assert mn[1].shape() == np.sum(sim_groupby.mask[1].data == 1) 17 | reg = sim_groupby.apply("regress") 18 | assert len(sim_groupby) == len(mn) 19 | 20 | 21 | def test_combine(sim_groupby): 22 | mn = sim_groupby.apply("mean") 23 | combine_mn = sim_groupby.combine(mn) 24 | assert len(combine_mn.shape()) == 1 25 | -------------------------------------------------------------------------------- /nltools/tests/test_mask.py: -------------------------------------------------------------------------------- 1 | from nltools.mask import create_sphere, roi_to_brain 2 | from nltools.data import Brain_Data 3 | import numpy as np 4 | import pandas as pd 5 | 6 | 7 | def test_create_sphere(): 8 | # Test values update to reflect the fact that standard Brain_Data mask has few voxels because ventricles are 0'd out 9 | 10 | a = create_sphere(radius=10, coordinates=[0, 0, 0]) 11 | assert np.sum(a.get_fdata()) >= 497 # 515 12 | a = create_sphere(radius=[10, 5], coordinates=[[0, 0, 0], [15, 0, 25]]) 13 | assert np.sum(a.get_fdata()) >= 553 # 571 14 | a = create_sphere(radius=10, coordinates=[[0, 0, 0], [15, 0, 25]]) 15 | assert np.sum(a.get_fdata()) >= 1013 # 1051 16 | 17 | 18 | def test_roi_to_brain(): 19 | s1 = create_sphere([15, 10, -8], radius=10) 20 | s2 = create_sphere([-15, 10, -8], radius=10) 21 | s3 = create_sphere([0, -15, -8], radius=10) 22 | masks = Brain_Data([s1, s2, s3]) 23 | 24 | d = [1, 2, 3] 25 | m = roi_to_brain(d, masks) 26 | assert np.all([np.any(m.data == x) for x in d]) 27 | 28 | d = pd.Series([1.1, 2.1, 3.1]) 29 | m = roi_to_brain(d, masks) 30 | assert np.all([np.any(m.data == x) for x in d]) 31 | 32 | d = np.array([1, 2, 3]) 33 | m = roi_to_brain(d, masks) 34 | assert np.all([np.any(m.data == x) for x in d]) 35 | 36 | d = pd.DataFrame([np.ones(10) * x for x in [1, 2, 3]]) 37 | m = roi_to_brain(d, masks) 38 | assert len(m) == d.shape[1] 39 | assert np.all([np.any(m[0].data == x) for x in d[0]]) 40 | 41 | d = np.array([np.ones(10) * x for x in [1, 2, 3]]) 42 | m = roi_to_brain(d, masks) 43 | assert len(m) == d.shape[1] 44 | assert np.all([np.any(m[0].data == x) for x in d[0]]) 45 | -------------------------------------------------------------------------------- /nltools/tests/test_prefs.py: -------------------------------------------------------------------------------- 1 | from nltools.prefs import MNI_Template 2 | from nltools.data import Brain_Data 3 | import pytest 4 | 5 | 6 | def test_change_mni_resolution(): 7 | # Defaults 8 | brain = Brain_Data() 9 | assert brain.mask.affine[1, 1] == 2.0 10 | assert MNI_Template["resolution"] == "2mm" 11 | 12 | # -> 3mm 13 | MNI_Template["resolution"] = "3mm" 14 | new_brain = Brain_Data() 15 | assert new_brain.mask.affine[1, 1] == 3.0 16 | 17 | # switch back and test attribute setting 18 | MNI_Template.resolution = 2.0 # floats are cool 19 | assert MNI_Template["resolution"] == "2mm" 20 | 21 | newer_brain = Brain_Data() 22 | assert newer_brain.mask.affine[1, 1] == 2.0 23 | 24 | with pytest.raises(NotImplementedError): 25 | MNI_Template.resolution = 1 26 | -------------------------------------------------------------------------------- /nltools/tests/test_simulator.py: -------------------------------------------------------------------------------- 1 | from nltools.simulator import Simulator, SimulateGrid 2 | import numpy as np 3 | 4 | 5 | def test_simulator(tmpdir): 6 | sim = Simulator() 7 | r = 10 8 | sigma = 1 9 | y = [0, 1] 10 | n_reps = 3 11 | output_dir = str(tmpdir) 12 | shape = (91, 109, 91) 13 | dat = sim.create_data(y, sigma, reps=n_reps, output_dir=None) 14 | assert len(dat) == n_reps * len(y) 15 | assert len(dat.Y) == n_reps * len(y) 16 | 17 | 18 | def test_simulategrid_fpr(tmpdir): 19 | grid_width = 10 20 | n_subjects = 25 21 | n_simulations = 100 22 | thresh = 0.05 23 | bonferroni_threshold = thresh / (grid_width**2) 24 | simulation = SimulateGrid( 25 | grid_width=grid_width, n_subjects=n_subjects, random_state=0 26 | ) 27 | simulation.plot_grid_simulation( 28 | threshold=bonferroni_threshold, threshold_type="p", n_simulations=n_simulations 29 | ) 30 | 31 | assert simulation.isfit 32 | assert simulation.grid_width == grid_width 33 | assert simulation.p_values.shape == (grid_width, grid_width) 34 | assert simulation.thresholded.shape == (grid_width, grid_width) 35 | assert simulation.fp_percent <= bonferroni_threshold 36 | assert len(simulation.multiple_fp) == n_simulations 37 | assert np.sum(simulation.multiple_fp > 0) / n_simulations <= (thresh + 0.03) 38 | 39 | 40 | def test_simulategrid_fdr(tmpdir): 41 | grid_width = 100 42 | n_subjects = 25 43 | n_simulations = 100 44 | thresh = 0.05 45 | signal_amplitude = 1 46 | signal_width = 10 47 | simulation = SimulateGrid( 48 | signal_amplitude=signal_amplitude, 49 | signal_width=signal_width, 50 | grid_width=grid_width, 51 | n_subjects=n_subjects, 52 | random_state=0, 53 | ) 54 | simulation.plot_grid_simulation( 55 | threshold=thresh, 56 | threshold_type="q", 57 | n_simulations=n_simulations, 58 | correction="fdr", 59 | ) 60 | 61 | assert len(simulation.multiple_fdr) == n_simulations 62 | assert np.mean(simulation.multiple_fdr) < thresh 63 | assert simulation.signal_width == signal_width 64 | assert simulation.correction == "fdr" 65 | -------------------------------------------------------------------------------- /nltools/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from nltools.utils import check_brain_data, check_brain_data_is_single 2 | from nltools.mask import create_sphere 3 | from nltools.data import Brain_Data 4 | import numpy as np 5 | 6 | 7 | def test_check_brain_data(sim_brain_data): 8 | mask = Brain_Data(create_sphere([15, 10, -8], radius=10)) 9 | a = check_brain_data(sim_brain_data) 10 | assert isinstance(a, Brain_Data) 11 | b = check_brain_data(sim_brain_data, mask=mask) 12 | assert isinstance(b, Brain_Data) 13 | assert b.shape()[1] == np.sum(mask.data == 1) 14 | 15 | 16 | def test_check_brain_data_is_single(sim_brain_data): 17 | assert not check_brain_data_is_single(sim_brain_data) 18 | assert check_brain_data_is_single(sim_brain_data[0]) 19 | -------------------------------------------------------------------------------- /nltools/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | NeuroLearn Utilities 3 | ==================== 4 | 5 | handy utilities. 6 | 7 | """ 8 | __all__ = [ 9 | "get_resource_path", 10 | "get_anatomical", 11 | "set_algorithm", 12 | "attempt_to_import", 13 | "all_same", 14 | "concatenate", 15 | "_bootstrap_apply_func", 16 | "set_decomposition_algorithm", 17 | ] 18 | __author__ = ["Luke Chang"] 19 | __license__ = "MIT" 20 | 21 | from os.path import dirname, join, sep as pathsep 22 | import nibabel as nib 23 | import importlib 24 | import os 25 | from sklearn.pipeline import Pipeline 26 | from sklearn.utils import check_random_state 27 | from scipy.spatial.distance import squareform 28 | import numpy as np 29 | import pandas as pd 30 | import collections 31 | from types import GeneratorType 32 | from h5py import File as h5File 33 | 34 | 35 | def to_h5(obj, file_name, obj_type="brain_data", h5_compression="gzip"): 36 | """User a combination of pandas and h5py to save objects to h5 files. Replaces 37 | deepdish. File loading is handled by class-specific methods""" 38 | 39 | if obj_type not in ["brain_data", "adjacency"]: 40 | raise TypeError("obj_type must be one of 'brain_data' or 'adjacency'") 41 | 42 | if obj_type == "brain_data": 43 | with pd.HDFStore(file_name, "w") as f: 44 | f["X"] = obj.X 45 | f["Y"] = obj.Y 46 | 47 | with h5File(file_name, "a") as f: 48 | f.create_dataset("data", data=obj.data, compression=h5_compression) 49 | f.create_dataset( 50 | "mask_affine", data=obj.mask.affine, compression=h5_compression 51 | ) 52 | f.create_dataset( 53 | "mask_data", data=obj.mask.get_fdata(), compression=h5_compression 54 | ) 55 | f.create_dataset("mask_file_name", data=obj.mask.get_filename()) 56 | else: 57 | with pd.HDFStore(file_name, "w") as f: 58 | f["Y"] = obj.Y 59 | 60 | with h5File(file_name, "a") as f: 61 | f.create_dataset("data", data=obj.data, compression=h5_compression) 62 | f.create_dataset("matrix_type", data=obj.matrix_type) 63 | f.create_dataset("issymmetric", data=obj.issymmetric) 64 | f.create_dataset("labels", data=obj.labels) 65 | f.create_dataset("is_single_matrix", data=obj.is_single_matrix) 66 | 67 | 68 | def get_resource_path(): 69 | """Get path to nltools resource directory.""" 70 | return join(dirname(__file__), "resources") + pathsep 71 | 72 | 73 | def get_anatomical(): 74 | """Get nltools default anatomical image. 75 | DEPRECATED. See MNI_Template and resolve_mni_path from nltools.prefs 76 | """ 77 | return nib.load(os.path.join(get_resource_path(), "MNI152_T1_2mm.nii.gz")) 78 | 79 | 80 | def get_mni_from_img_resolution(brain, img_type="plot"): 81 | """ 82 | Get the path to the resolution MNI anatomical image that matches the resolution of a Brain_Data instance. Used by Brain_Data.plot() and .iplot() to set backgrounds appropriately. 83 | 84 | Args: 85 | brain: Brain_Data instance 86 | 87 | Returns: 88 | file_path: path to MNI image 89 | """ 90 | 91 | if img_type not in ["plot", "brain"]: 92 | raise ValueError("img_type must be 'plot' or 'brain' ") 93 | 94 | res_array = np.abs(np.diag(brain.nifti_masker.affine_)[:3]) 95 | voxel_dims = np.unique(abs(res_array)) 96 | if len(voxel_dims) != 1: 97 | raise ValueError( 98 | "Voxels are not isometric and cannot be visualized in standard space" 99 | ) 100 | else: 101 | dim = str(int(voxel_dims[0])) + "mm" 102 | if img_type == "brain": 103 | mni = f"MNI152_T1_{dim}_brain.nii.gz" 104 | else: 105 | mni = f"MNI152_T1_{dim}.nii.gz" 106 | return os.path.join(get_resource_path(), mni) 107 | 108 | 109 | def set_algorithm(algorithm, *args, **kwargs): 110 | """Setup the algorithm to use in subsequent prediction analyses. 111 | 112 | Args: 113 | algorithm: The prediction algorithm to use. Either a string or an 114 | (uninitialized) scikit-learn prediction object. If string, 115 | must be one of 'svm','svr', linear','logistic','lasso', 116 | 'lassopcr','lassoCV','ridge','ridgeCV','ridgeClassifier', 117 | 'randomforest', or 'randomforestClassifier' 118 | kwargs: Additional keyword arguments to pass onto the scikit-learn 119 | clustering object. 120 | 121 | Returns: 122 | predictor_settings: dictionary of settings for prediction 123 | 124 | """ 125 | 126 | # NOTE: function currently located here instead of analysis.py to avoid circular imports 127 | 128 | predictor_settings = {} 129 | predictor_settings["algorithm"] = algorithm 130 | 131 | def load_class(import_string): 132 | class_data = import_string.split(".") 133 | module_path = ".".join(class_data[:-1]) 134 | class_str = class_data[-1] 135 | module = importlib.import_module(module_path) 136 | return getattr(module, class_str) 137 | 138 | algs_classify = { 139 | "svm": "sklearn.svm.SVC", 140 | "logistic": "sklearn.linear_model.LogisticRegression", 141 | "ridgeClassifier": "sklearn.linear_model.RidgeClassifier", 142 | "ridgeClassifierCV": "sklearn.linear_model.RidgeClassifierCV", 143 | "randomforestClassifier": "sklearn.ensemble.RandomForestClassifier", 144 | } 145 | algs_predict = { 146 | "svr": "sklearn.svm.SVR", 147 | "linear": "sklearn.linear_model.LinearRegression", 148 | "lasso": "sklearn.linear_model.Lasso", 149 | "lassoCV": "sklearn.linear_model.LassoCV", 150 | "ridge": "sklearn.linear_model.Ridge", 151 | "ridgeCV": "sklearn.linear_model.RidgeCV", 152 | "randomforest": "sklearn.ensemble.RandomForest", 153 | } 154 | 155 | if algorithm in algs_classify.keys(): 156 | predictor_settings["prediction_type"] = "classification" 157 | alg = load_class(algs_classify[algorithm]) 158 | predictor_settings["predictor"] = alg(*args, **kwargs) 159 | elif algorithm in algs_predict: 160 | predictor_settings["prediction_type"] = "prediction" 161 | alg = load_class(algs_predict[algorithm]) 162 | predictor_settings["predictor"] = alg(*args, **kwargs) 163 | elif algorithm == "lassopcr": 164 | predictor_settings["prediction_type"] = "prediction" 165 | from sklearn.linear_model import Lasso 166 | from sklearn.decomposition import PCA 167 | 168 | predictor_settings["_lasso"] = Lasso() 169 | predictor_settings["_pca"] = PCA() 170 | predictor_settings["predictor"] = Pipeline( 171 | steps=[ 172 | ("pca", predictor_settings["_pca"]), 173 | ("lasso", predictor_settings["_lasso"]), 174 | ] 175 | ) 176 | elif algorithm == "pcr": 177 | predictor_settings["prediction_type"] = "prediction" 178 | from sklearn.linear_model import LinearRegression 179 | from sklearn.decomposition import PCA 180 | 181 | predictor_settings["_regress"] = LinearRegression() 182 | predictor_settings["_pca"] = PCA() 183 | predictor_settings["predictor"] = Pipeline( 184 | steps=[ 185 | ("pca", predictor_settings["_pca"]), 186 | ("regress", predictor_settings["_regress"]), 187 | ] 188 | ) 189 | else: 190 | raise ValueError( 191 | """Invalid prediction/classification algorithm name. 192 | Valid options are 'svm','svr', 'linear', 'logistic', 'lasso', 193 | 'lassopcr','lassoCV','ridge','ridgeCV','ridgeClassifier', 194 | 'randomforest', or 'randomforestClassifier'.""" 195 | ) 196 | 197 | return predictor_settings 198 | 199 | 200 | def set_decomposition_algorithm(algorithm, n_components=None, *args, **kwargs): 201 | """Setup the algorithm to use in subsequent decomposition analyses. 202 | 203 | Args: 204 | algorithm: The decomposition algorithm to use. Either a string or an 205 | (uninitialized) scikit-learn decomposition object. 206 | If string must be one of 'pca','nnmf', ica','fa', 207 | 'dictionary', 'kernelpca'. 208 | kwargs: Additional keyword arguments to pass onto the scikit-learn 209 | clustering object. 210 | 211 | Returns: 212 | predictor_settings: dictionary of settings for prediction 213 | 214 | """ 215 | 216 | # NOTE: function currently located here instead of analysis.py to avoid circular imports 217 | 218 | def load_class(import_string): 219 | class_data = import_string.split(".") 220 | module_path = ".".join(class_data[:-1]) 221 | class_str = class_data[-1] 222 | module = importlib.import_module(module_path) 223 | return getattr(module, class_str) 224 | 225 | algs = { 226 | "pca": "sklearn.decomposition.PCA", 227 | "ica": "sklearn.decomposition.FastICA", 228 | "nnmf": "sklearn.decomposition.NMF", 229 | "fa": "sklearn.decomposition.FactorAnalysis", 230 | "dictionary": "sklearn.decomposition.DictionaryLearning", 231 | "kernelpca": "sklearn.decomposition.KernelPCA", 232 | } 233 | 234 | if algorithm in algs.keys(): 235 | alg = load_class(algs[algorithm]) 236 | alg = alg(n_components, *args, **kwargs) 237 | else: 238 | raise ValueError( 239 | """Invalid prediction/classification algorithm name. 240 | Valid options are 'pca','ica', 'nnmf', 'fa'""" 241 | ) 242 | return alg 243 | 244 | 245 | def isiterable(obj): 246 | """Returns True if the object is one of allowable iterable types.""" 247 | return isinstance(obj, (list, tuple, GeneratorType)) 248 | 249 | 250 | module_names = {} 251 | Dependency = collections.namedtuple("Dependency", "package value") 252 | 253 | 254 | def attempt_to_import(dependency, name=None, fromlist=None): 255 | if name is None: 256 | name = dependency 257 | try: 258 | mod = __import__(dependency, fromlist=fromlist) 259 | except ImportError: 260 | mod = None 261 | module_names[name] = Dependency(dependency, mod) 262 | return mod 263 | 264 | 265 | def all_same(items): 266 | return np.all(x == items[0] for x in items) 267 | 268 | 269 | def concatenate(data): 270 | """Concatenate a list of Brain_Data() or Adjacency() objects""" 271 | 272 | if not isinstance(data, list): 273 | raise ValueError("Make sure you are passing a list of objects.") 274 | 275 | if all([isinstance(x, data[0].__class__) for x in data]): 276 | # Temporarily Removing this for circular imports (LC) 277 | # if not isinstance(data[0], (Brain_Data, Adjacency)): 278 | # raise ValueError('Make sure you are passing a list of Brain_Data' 279 | # ' or Adjacency objects.') 280 | 281 | out = data[0].__class__() 282 | for i in data: 283 | out = out.append(i) 284 | else: 285 | raise ValueError("Make sure all objects in the list are the same type.") 286 | return out 287 | 288 | 289 | def _bootstrap_apply_func(data, function, random_state=None, *args, **kwargs): 290 | """Bootstrap helper function. Sample with replacement and apply function""" 291 | random_state = check_random_state(random_state) 292 | data_row_id = range(data.shape()[0]) 293 | new_dat = data[ 294 | random_state.choice(data_row_id, size=len(data_row_id), replace=True) 295 | ] 296 | return getattr(new_dat, function)(*args, **kwargs) 297 | 298 | 299 | def check_square_numpy_matrix(data): 300 | """Helper function to make sure matrix is square and numpy array""" 301 | 302 | from nltools.data import Adjacency 303 | 304 | if isinstance(data, Adjacency): 305 | data = data.squareform() 306 | elif isinstance(data, pd.DataFrame): 307 | data = data.values 308 | else: 309 | data = np.array(data) 310 | 311 | if len(data.shape) != 2: 312 | try: 313 | data = squareform(data) 314 | except ValueError: 315 | raise ValueError( 316 | "Array does not contain the correct number of elements to be square" 317 | ) 318 | return data 319 | 320 | 321 | def check_brain_data(data, mask=None): 322 | """Check if data is a Brain_Data Instance.""" 323 | from nltools.data import Brain_Data 324 | 325 | if not isinstance(data, Brain_Data): 326 | if isinstance(data, nib.Nifti1Image): 327 | data = Brain_Data(data, mask=mask) 328 | else: 329 | raise ValueError("Make sure data is a Brain_Data instance.") 330 | else: 331 | if mask is not None: 332 | data = data.apply_mask(mask) 333 | return data 334 | 335 | 336 | def check_brain_data_is_single(data): 337 | """Logical test if Brain_Data instance is a single image 338 | 339 | Args: 340 | data: brain data 341 | 342 | Returns: 343 | (bool) 344 | 345 | """ 346 | data = check_brain_data(data) 347 | if len(data.shape()) > 1: 348 | return False 349 | else: 350 | return True 351 | 352 | 353 | def _roi_func(brain, roi, algorithm, cv_dict, **kwargs): 354 | """Brain_Data.predict_multi() helper function""" 355 | return brain.apply_mask(roi).predict( 356 | algorithm=algorithm, cv_dict=cv_dict, plot=False, **kwargs 357 | ) 358 | 359 | 360 | class AmbiguityError(Exception): 361 | pass 362 | 363 | 364 | def generate_jitter(n_trials, mean_time=5, min_time=2, max_time=12, atol=0.2): 365 | """Generate jitter from exponential distribution with constraints 366 | 367 | Draws from exponential distribution until the distribution satisfies the constraints: 368 | np.abs(np.mean(min_time > data < max_time) - mean_time) <= atol 369 | 370 | Args: 371 | n_trials: (int) number of trials to generate jitter 372 | mean_time: (float) desired mean of distribution 373 | min_time: (float) desired min of distribution 374 | max_time: (float) desired max of distribution 375 | atol: (float) precision of deviation from mean 376 | 377 | Returns: 378 | data: (np.array) jitter for each trial 379 | 380 | """ 381 | 382 | def generate_data(n_trials, scale=5, min_time=2, max_time=12): 383 | data = [] 384 | i = 0 385 | while i < n_trials: 386 | datam = np.random.exponential(scale=5) 387 | if (datam > min_time) & (datam < max_time): 388 | data.append(datam) 389 | i += 1 390 | return data 391 | 392 | mean_diff = False 393 | while ~mean_diff: 394 | data = generate_data(n_trials, min_time=min_time, max_time=max_time) 395 | mean_diff = np.isclose(np.mean(data), mean_time, rtol=0, atol=atol) 396 | return data 397 | -------------------------------------------------------------------------------- /nltools/version.py: -------------------------------------------------------------------------------- 1 | """Specifies current version of nltools to be used by setup.py and __init__.py 2 | """ 3 | 4 | __version__ = "0.5.1" 5 | -------------------------------------------------------------------------------- /optional-dependencies.txt: -------------------------------------------------------------------------------- 1 | requests 2 | networkx 3 | ipywidgets>=5.2.2 4 | statsmodels>=0.9.0 5 | tables 6 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs = env -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | pytest 3 | pytest-sugar 4 | black==23.3.0 5 | coveralls 6 | sphinx 7 | sphinx_gallery 8 | sphinx_bootstrap_theme 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nibabel>=3.0.1 2 | scikit-learn>=0.21.0 3 | nilearn>=0.6.0 4 | pandas>=1.1.0 5 | numpy<1.24 6 | seaborn>=0.7.0 7 | matplotlib>=2.2.0 8 | scipy>=1.7.0 9 | pynv 10 | joblib>=0.15 11 | h5py 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [bdist_wheel] 5 | universal=1 6 | 7 | [check-manifest] 8 | ignore = 9 | .travis.yml 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | version = {} 4 | with open("nltools/version.py") as f: 5 | exec(f.read(), version) 6 | 7 | with open("requirements.txt") as f: 8 | requirements = f.read().splitlines() 9 | 10 | extra_setuptools_args = dict(tests_require=["pytest"]) 11 | 12 | setup( 13 | name="nltools", 14 | version=version["__version__"], 15 | author="Cosan Lab", 16 | author_email="luke.j.chang@dartmouth.edu", 17 | url="https://cosanlab.github.io/nltools", 18 | python_requires=">=3.8", 19 | install_requires=requirements, 20 | extras_require={"interactive_plots": ["ipywidgets>=5.2.2"]}, 21 | packages=find_packages(exclude=["nltools/tests"]), 22 | package_data={"nltools": ["resources/*"]}, 23 | include_package_data=True, 24 | license="LICENSE.txt", 25 | description="A Python package to analyze neuroimaging data", 26 | long_description="nltools is a collection of python tools to perform " 27 | "preprocessing, univariate GLMs, and predictive " 28 | "multivariate modeling of neuroimaging data. It is the " 29 | "analysis engine powering www.neuro-learn.org.", 30 | keywords=["neuroimaging", "preprocessing", "analysis", "machine-learning"], 31 | classifiers=[ 32 | "Programming Language :: Python :: 3.8", 33 | "Programming Language :: Python :: 3.9", 34 | "Programming Language :: Python :: 3.10", 35 | "Programming Language :: Python :: 3.11", 36 | "Operating System :: OS Independent", 37 | "Intended Audience :: Science/Research", 38 | "License :: OSI Approved :: MIT License", 39 | ], 40 | **extra_setuptools_args 41 | ) 42 | --------------------------------------------------------------------------------