├── tests
├── __init__.py
├── backtests
│ ├── __init__.py
│ └── test_compare_outputs.py
└── unittests
│ ├── __init__.py
│ ├── test_processing_steps.py
│ ├── test_part_of_two_in_three.py
│ ├── test_seven_point_one_side_mean.py
│ ├── test_two_in_three.py
│ ├── test_seven_point_trend.py
│ ├── test_limits_calculations.py
│ ├── test_part_of_seven_trend.py
│ ├── test_special_cause_flag.py
│ └── test_pandas_spc_x_calc.py
├── reports
└── ae_attendance.qmd
├── docs
├── .gitignore
├── _site
│ ├── robots.txt
│ ├── posts
│ │ ├── welcome
│ │ │ └── thumbnail.png
│ │ └── rap-maturity
│ │ │ └── nhsd-rap.png
│ ├── site_libs
│ │ ├── bootstrap
│ │ │ └── bootstrap-icons.woff
│ │ ├── quarto-html
│ │ │ ├── tippy.css
│ │ │ ├── quarto-syntax-highlighting.css
│ │ │ └── anchor.min.js
│ │ ├── quarto-nav
│ │ │ └── quarto-nav.js
│ │ ├── quarto-listing
│ │ │ └── quarto-listing.js
│ │ └── clipboard
│ │ │ └── clipboard.min.js
│ ├── listings.json
│ ├── sitemap.xml
│ ├── updates.xml
│ ├── index.css
│ ├── _assets
│ │ └── style
│ │ │ └── styles.css
│ └── documentation
│ │ └── index.html
├── _assets
│ ├── favicon.ico
│ ├── style
│ │ ├── js.html
│ │ ├── theme.scss
│ │ └── styles.css
│ └── img
│ │ └── iconfinder_Rubics_Cube.svg
├── posts
│ ├── welcome
│ │ ├── thumbnail.png
│ │ └── index.qmd
│ ├── rap-maturity
│ │ ├── nhsd-rap.png
│ │ └── index.qmd
│ └── _metadata.yml
├── documentation
│ ├── index.qmd
│ └── pandas_spc_x_calc.qmd
├── faq.qmd
├── tutorials
│ ├── pandas.qmd
│ └── index.qmd
├── updates.qmd
├── about.qmd
├── contribute.qmd
├── _quarto.yml
├── index.qmd
├── _freeze
│ └── site_libs
│ │ └── clipboard
│ │ └── clipboard.min.js
└── index.css
├── nhspy_plotthedots
├── __init__.py
├── utilities
│ ├── __init__.py
│ ├── field_definitions.py
│ ├── processing_steps.py
│ └── data_connections.py
├── params.py
├── create_publication.py
├── spc_calculations.py
├── plotly_spc_chart.py
└── pandas_spc_calculations.py
├── logo.png
├── requirements.txt
├── environment.yml
├── setup.py
├── .github
├── dependabot.yml
└── workflows
│ ├── codecov.yml
│ ├── quarto_publish.yml
│ ├── pytest_flake8.yml
│ └── codeql.yml
├── pyproject.toml
├── LICENSE
├── .gitignore
└── README.md
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/reports/ae_attendance.qmd:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | /.quarto/
2 |
--------------------------------------------------------------------------------
/tests/backtests/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tests/unittests/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/nhspy_plotthedots/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/nhspy_plotthedots/utilities/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nhs-oa-community/nhspy-plotthedots/HEAD/logo.png
--------------------------------------------------------------------------------
/docs/_site/robots.txt:
--------------------------------------------------------------------------------
1 | Sitemap: https://nhs-pycom.github.io/nhspy-plotthedots/sitemap.xml
2 |
--------------------------------------------------------------------------------
/tests/backtests/test_compare_outputs.py:
--------------------------------------------------------------------------------
1 | # Compares historical data outputs between RAP and non RAP pipelines.
2 |
--------------------------------------------------------------------------------
/docs/_assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nhs-oa-community/nhspy-plotthedots/HEAD/docs/_assets/favicon.ico
--------------------------------------------------------------------------------
/docs/posts/welcome/thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nhs-oa-community/nhspy-plotthedots/HEAD/docs/posts/welcome/thumbnail.png
--------------------------------------------------------------------------------
/docs/posts/rap-maturity/nhsd-rap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nhs-oa-community/nhspy-plotthedots/HEAD/docs/posts/rap-maturity/nhsd-rap.png
--------------------------------------------------------------------------------
/docs/_site/posts/welcome/thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nhs-oa-community/nhspy-plotthedots/HEAD/docs/_site/posts/welcome/thumbnail.png
--------------------------------------------------------------------------------
/docs/documentation/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Documentation"
3 | comments: false
4 | ---
5 |
6 | ::: {.callout-warning}
7 | ## Under Development
8 | :::
--------------------------------------------------------------------------------
/docs/_site/posts/rap-maturity/nhsd-rap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nhs-oa-community/nhspy-plotthedots/HEAD/docs/_site/posts/rap-maturity/nhsd-rap.png
--------------------------------------------------------------------------------
/docs/faq.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Frequently Asked Questions"
3 | comments: false
4 | ---
5 |
6 | ::: {.callout-warning}
7 | ## Under Development
8 | :::
9 |
--------------------------------------------------------------------------------
/docs/_site/site_libs/bootstrap/bootstrap-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nhs-oa-community/nhspy-plotthedots/HEAD/docs/_site/site_libs/bootstrap/bootstrap-icons.woff
--------------------------------------------------------------------------------
/docs/tutorials/pandas.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Pandas"
3 | subtitle: "Working with pandas"
4 | comments: false
5 | ---
6 |
7 | ::: {.callout-warning}
8 | ## Under Development
9 | :::
--------------------------------------------------------------------------------
/docs/_site/listings.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "listing": "/updates.html",
4 | "items": [
5 | "/posts/rap-maturity/index.html",
6 | "/posts/welcome/index.html"
7 | ]
8 | }
9 | ]
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # The libraries used by your code should be listed here
2 | # See https://github.com/NHSDigital/rap-community-of-practice/blob/main/python/project-structure-and-packaging.md
3 |
4 | pandas
5 | numpy
6 | plotly
--------------------------------------------------------------------------------
/docs/documentation/pandas_spc_x_calc.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "pandas_spc_x_calc"
3 | subtitle: "Calculates the SPC for a given DataFrame with a set column of values"
4 | comments: false
5 | ---
6 |
7 | ::: {.callout-warning}
8 | ## Under Development
9 | :::
--------------------------------------------------------------------------------
/docs/posts/_metadata.yml:
--------------------------------------------------------------------------------
1 | # options specified here will apply to all posts in this folder
2 |
3 | # freeze computational output
4 | # (see https://quarto.org/docs/projects/code-execution.html#freeze)
5 | freeze: true
6 |
7 | # Enable banner style title blocks
8 | title-block-banner: true
9 |
--------------------------------------------------------------------------------
/docs/updates.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Updates"
3 | subtitle: "News, tips, and commentary"
4 | listing:
5 | sort: "date desc"
6 | contents: "posts"
7 | sort-ui: false
8 | filter-ui: false
9 | categories: true
10 | feed: true
11 | page-layout: full
12 | comments: false
13 | ---
--------------------------------------------------------------------------------
/nhspy_plotthedots/params.py:
--------------------------------------------------------------------------------
1 | """
2 | Purpose of script: contains all of the parameters that we expect to change
3 | frequently, e.g. input data.
4 | """
5 | params = {
6 | 'publication_version': 'This message shows that you have successfully imported params from the params module'
7 | }
8 |
--------------------------------------------------------------------------------
/docs/_assets/style/js.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/environment.yml:
--------------------------------------------------------------------------------
1 | #A template of the conda environment
2 | #The template contains commonly used packages
3 | #For details: https://github.com/NHSDigital/rap-community-of-practice/blob/main/python/virtual-environments.md
4 | name: nhspy-plotthedots
5 |
6 | channels:
7 | - defaults
8 |
9 | dependencies:
10 | - flake8=3.9.0
11 | - openpyxl
12 | - pip==20.2.4
13 | - python=3.9
14 | - pyodbc
15 | - ipykernel
16 | - ipython
17 | - nbconvert==6.0.7
18 | - numpy==1.19.5
19 | - pandas
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """
2 | Template of setup.py.
3 |
4 | See https://github.com/NHSDigital/rap-community-of-practice/blob/main/python/project-structure-and-packaging.md
5 | """
6 |
7 | from setuptools import find_packages, setup
8 |
9 | setup(
10 | name='nhspy-plotthedots',
11 | packages=find_packages(),
12 | version='0.1.6',
13 | description='Python SPC chart tool',
14 | author='NHS Python Community',
15 | license='MIT',
16 | setup_requires=['pytest-runner','flake8'],
17 | tests_require=['pytest'],
18 | )
19 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "pip" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/docs/_assets/style/theme.scss:
--------------------------------------------------------------------------------
1 | /*-- scss:defaults --*/
2 | $link-color: #39729E;
3 | $text-muted: #6a737b;
4 |
5 | /*-- scss:rules --*/
6 |
7 | .layout-example {
8 | background: $gray-500;
9 | color: $white;
10 | text-align: center;
11 | margin-bottom: 1em;
12 | font-family: $font-family-monospace;
13 | font-size: .875em;
14 | font-weight: 600;
15 | padding-top: 1em;
16 | border-radius: 3px;
17 | }
18 |
19 | .left {
20 | text-align: left;
21 | padding-left: 1em;
22 | }
23 |
24 | .right {
25 | text-align: right;
26 | padding-right: 1em;
27 | }
28 |
29 | .hello-quarto-banner h1 {
30 | margin-top: 0;
31 | margin-bottom: 0.5rem;
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/tests/unittests/test_processing_steps.py:
--------------------------------------------------------------------------------
1 | """
2 | Template of a unit test script
3 |
4 |
5 | To run this test requires the following step:
6 | 1) Open Anaconda prompt and set the directory to the project folder
7 | 2) Enter:
8 | >pytest
9 | and run
10 | """
11 |
12 | import pytest
13 |
14 | from nhspy_plotthedots.utilities import processing_steps
15 |
16 | def test_derived_something():
17 | """
18 | A template to create a pytest def
19 |
20 | """
21 | ground_truth = 'output'
22 |
23 | output_from_function = processing_steps.derived_something()
24 |
25 | assert ground_truth == output_from_function
--------------------------------------------------------------------------------
/.github/workflows/codecov.yml:
--------------------------------------------------------------------------------
1 | name: code coverage
2 | on: [pull_request]
3 | jobs:
4 | run:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - name: Checkout
8 | uses: actions/checkout@v2
9 | - name: Set up Python 3.10
10 | uses: actions/setup-python@v2
11 | with:
12 | python-version: '3.10'
13 | - name: Install dependencies
14 | run: |
15 | python -m pip install --upgrade pip
16 | pip install pytest-cov pytest
17 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
18 | - name: Run tests and collect coverage
19 | run: pytest --cov nhspy_plotthedots
20 | - name: Upload coverage to Codecov
21 | uses: codecov/codecov-action@v3
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "nhspy-plotthedots-test"
7 | version = "0.1.6"
8 | authors = [
9 | { name="Craig R Shenton", email="craig.shenton@nhs.net" },
10 | { name="Tom Jemmett", email="tom.jemmett@gmail.com" },
11 | ]
12 | description = "nhspy-plotthedots: Draw XmR charts for NHSE 'Making Data Count' programme"
13 | readme = "README.md"
14 | requires-python = ">=3.7"
15 | classifiers = [
16 | "Programming Language :: Python :: 3",
17 | "License :: OSI Approved :: MIT License",
18 | "Operating System :: OS Independent",
19 | ]
20 |
21 | [project.urls]
22 | "Homepage" = "https://github.com/nhs-pycom/nhspy-plotthedots"
23 | "Bug Tracker" = "https://github.com/nhs-pycom/nhspy-plotthedots/issues"
--------------------------------------------------------------------------------
/nhspy_plotthedots/utilities/field_definitions.py:
--------------------------------------------------------------------------------
1 | """
2 | Purpose of the script: contains the definitions for each of the fields
3 | (columns) derived in the process. By abstracting these definitions out of the
4 | code and making them reuseable, we achieve some great benefits. First, it
5 | becomes much easier to maintain. When the specifications change next year, we
6 | only need to make the change in one location. Next, it becomes much easier to
7 | test. We write unit tests for each of these definitions and can then reuse
8 | these definitions in many places without increasing risk.
9 |
10 | For more information see https://github.com/NHSDigital/rap-community-of-practice/blob/d8ffe2decd737c594222565423fe31d87d590a12/development-approach/05_unit-testing-field-definitions.md#field-definitions-and-how-to-use-them
11 | """
12 | aggregate_fields = (None)
--------------------------------------------------------------------------------
/docs/about.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "About {nhspy-plotthedots}"
3 | comments: false
4 | ---
5 |
6 | ## Authors
7 |
8 | - [Craig Robert Shenton, PhD](https://github.com/craig-shenton) `author`, `maintainer`.
9 | - [Tom Jemmett](https://github.com/tomjemmett) `author`.
10 |
11 | ## Licence
12 |
13 | MIT License, see [`LICENSE.md`](https://github.com/nhs-pycom/nhspy-plotthedots/blob/main/LICENSE)
14 |
15 | ## Citation
16 |
17 | NHS Python Community *nhspy-plotthedots: Draw XmR charts in python for NHSE 'Making Data Count' programme* (2023). At <[https://github.com/nhs-pycom/nhspy-plotthedots](https://github.com/nhs-pycom/nhspy-plotthedots)>.
18 |
19 | ```r
20 | @Manual{nhspyplotthedots2023,
21 | title = {nhspy-plotthedots: Draw XmR charts in python for NHSE 'Making Data Count' programme},
22 | author = {NHS Python Community},
23 | url = {https://github.com/nhs-pycom/nhspy-plotthedots},
24 | year = {2023},
25 | }
26 | ```
27 |
28 |
--------------------------------------------------------------------------------
/.github/workflows/quarto_publish.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches: main
4 | paths: 'docs/**'
5 | pull_request:
6 | branches: main
7 | paths: 'docs/**'
8 |
9 | name: Render and Publish
10 |
11 | jobs:
12 | build-deploy:
13 | runs-on: ubuntu-latest
14 | permissions:
15 | contents: write
16 | steps:
17 | - name: Check out repository
18 | uses: actions/checkout@v3
19 |
20 | - name: Set up Quarto
21 | uses: quarto-dev/quarto-actions/setup@v2
22 |
23 | - name: Install Python and Dependencies
24 | uses: actions/setup-python@v4
25 | with:
26 | python-version: '3.10'
27 | cache: 'pip'
28 | - run: pip install jupyter
29 | - run: pip install -r requirements.txt
30 |
31 | - name: Render and Publish
32 | uses: quarto-dev/quarto-actions/publish@v2
33 | with:
34 | target: gh-pages
35 | path: docs/
36 | env:
37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/docs/posts/welcome/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "nhspy-plotthedots alpha release"
3 | author: "Craig R Shenton"
4 | date: "2023-01-22"
5 | categories: [release]
6 | image: "thumbnail.png"
7 | description: "v0.1.6 (alpha) now available on test pipy"
8 | title-block-banner: false
9 | ---
10 |
11 | We are excited to announce the release of `nhspy-plotthedots` alpha, version `0.1.6`. This new package is now available on test PyPI at [https://test.pypi.org/project/nhspy-plotthedots-test/](https://test.pypi.org/project/nhspy-plotthedots-test/) for users to test and provide feedback.
12 |
13 | In addition to the package release, the documentation site for nhspy-plotthedots is now live. This quarto site will provide detailed information on how to use the package, including installation instructions, usage examples, and API reference.
14 |
15 | We look forward to hearing your feedback on `nhspy-plotthedots` and hope you find it as useful as we do.
16 |
17 | ::: {.callout-warning}
18 | ## Note
19 |
20 | This is an alpha release for testing, not for use in production.
21 | :::
--------------------------------------------------------------------------------
/nhspy_plotthedots/utilities/processing_steps.py:
--------------------------------------------------------------------------------
1 | """
2 | Purpose of script: contains the core business logic
3 | """
4 |
5 | def validation_checks():
6 | """
7 | A docstring template to describe the function.
8 |
9 | Currently this function prints out a message to indicates it has been
10 | successfully inported from preprocessing module
11 |
12 | Parameters
13 | ----------
14 | None
15 |
16 | Returns
17 | -------
18 | None.
19 |
20 | """
21 | print("This message shows that you have successfully imported \
22 | the validation_checks() function from the preprocessing module")
23 |
24 | def derived_something():
25 | """
26 | A docstring template to describe the function.
27 |
28 | This function is part of the unit test template
29 |
30 | Returns
31 | -------
32 | An string with the word output
33 |
34 | """
35 | print("This message shows that you have successfully imported \
36 | the derived_something() function from the preprocessing module")
37 |
38 | output_vaule = 'output'
39 |
40 | return output_vaule
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 NHS Python Community
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 |
--------------------------------------------------------------------------------
/nhspy_plotthedots/utilities/data_connections.py:
--------------------------------------------------------------------------------
1 | """
2 | Purpose of script: handles reading data in and writing data back out.
3 | """
4 |
5 | def get_df_from_sql():
6 | """
7 | A docstring template to describe the function.
8 |
9 | Currently this function prints out a message to indicates it has been
10 | successfully inported from data connections module
11 |
12 | Parameters
13 | ----------
14 | None
15 |
16 | Returns
17 | -------
18 | None.
19 |
20 | """
21 | print("This message shows that you have successfully imported \
22 | the get_df_from_sql() function from the data connections module")
23 |
24 | def write_df_to_sql():
25 | """
26 | A docstring template to describe the function.
27 |
28 | Currently this function prints out a message to indicates it has been
29 | successfully inported from data connections module
30 |
31 | Parameters
32 | ----------
33 | None
34 |
35 | Returns
36 | -------
37 | None.
38 |
39 | """
40 |
41 | print("This message shows that you have successfully imported \
42 | the write_df_to_sql() function from the data connections module")
43 |
44 |
--------------------------------------------------------------------------------
/.github/workflows/pytest_flake8.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3 |
4 | name: pytest and flake8
5 |
6 | on: [pull_request]
7 |
8 | permissions:
9 | contents: read
10 |
11 | jobs:
12 | build:
13 |
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v3
18 | - name: Set up Python 3.10
19 | uses: actions/setup-python@v3
20 | with:
21 | python-version: "3.10"
22 | - name: Install dependencies
23 | run: |
24 | python -m pip install --upgrade pip
25 | pip install flake8 pytest
26 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
27 | - name: Lint with flake8
28 | run: |
29 | # stop the build if there are Python syntax errors or undefined names
30 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
31 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
32 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
33 | - name: Test with pytest
34 | run: |
35 | pytest
36 |
--------------------------------------------------------------------------------
/docs/_site/site_libs/quarto-html/tippy.css:
--------------------------------------------------------------------------------
1 | .tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}
--------------------------------------------------------------------------------
/docs/_assets/img/iconfinder_Rubics_Cube.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nhspy_plotthedots/create_publication.py:
--------------------------------------------------------------------------------
1 | """
2 | Purpose of the script: organises the steps in a simple, easy-to-understand manner
3 | that should be readable by anyone, even if they don't know python. In this way,
4 | we aim to reduce risk by make the code accessible to new staff.
5 |
6 | In this template, we use the sys library for add inputs via command line
7 |
8 | To use sys, open anaconda prompt and point to the root folder. For example
9 | suppose I want to input 10 in the command line:
10 |
11 | > python .\my_project\create_publication.py 10
12 |
13 | The expected output would be:
14 | This message shows that you have successfully imported the get_df_from_sql() function from the data connections module
15 | This message shows that you have successfully imported the write_df_to_sql() function from the data connections module
16 | This message shows that you have successfully imported the validation_checks() function from the preprocessing module
17 | This message shows that you have successfully imported params from the params module
18 | user parameter: 10
19 |
20 | """
21 |
22 | # %%
23 | import sys
24 | from utilities import data_connections
25 | from utilities import processing_steps
26 | from params import params
27 |
28 | def main():
29 | # check if data_connections module has been imported
30 | data_connections.get_df_from_sql()
31 |
32 | data_connections.write_df_to_sql()
33 |
34 | # check if processing_steps module has been imported
35 | processing_steps.validation_checks()
36 |
37 | # check if params file has been imported
38 | print(params['publication_version'])
39 |
40 | # check if user command line parameters have been imported
41 | for arg in sys.argv[1:]:
42 | print('user parameter: ' + arg)
43 |
44 | if __name__ == "__main__":
45 | main()
--------------------------------------------------------------------------------
/docs/contribute.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: Contribute to the docs
3 | subtitle: "How to add a new page to the documentations site using Quarto?"
4 | ---
5 |
6 | ## How to contribute?
7 |
8 | ### Make a new branch of the repository
9 |
10 | ```bash
11 | git checkout -b
12 | ```
13 |
14 | ### Make a new `.qmd` (q-markdown) file in the `/docs` folder
15 |
16 | To the `.qmd` file add a YAML header with a title and subtitle.
17 | ```yaml
18 | ---
19 | title: Contribute to the docs
20 | subtitle: "How to add a new page to the documentations site using Quarto?"
21 | ---
22 | ```
23 |
24 | ### Add a link to your file to the `_quarto.yml` config file in `/docs`
25 |
26 | Open the `_quarto.yml` configuration file and find the `sidebar` config code.
27 | ```yaml
28 | sidebar:
29 | - id: nav
30 | style: "floating"
31 | collapse-level: 3
32 | align: left
33 | contents:
34 | ```
35 | Under the `contents:` object add a new section (if required), a string lable for your page, and a link to the `.qmd` file itself.
36 |
37 | ```yaml
38 | - section: "tutorials"
39 | contents:
40 | - text: "Reproducible Analytical Pipelines"
41 | file: tutorials/intro-to-rap.qmd
42 | ```
43 |
44 | ### Publish your changes to GitHub
45 |
46 | ::: {.callout-warning appearance="simple" collapse="false"}
47 | ### Render Quarto before publishing
48 |
49 | Remember to [render your changes locally using R-Studio](https://quarto.org/docs/tools/rstudio.html#render-and-preview) (or VScode) before publishing
50 | :::
51 |
52 | Commit your changes locally
53 |
54 | ```bash
55 | git commit -m 'Added new page to docs'
56 | ```
57 |
58 | Then push your changes to the remote branch
59 |
60 | ```bash
61 | git push origin
62 | ```
63 |
64 | Finally, open a Pull Request (PR) [https://github.com/nhs-pycom/nhspy-plotthedots/pulls](https://github.com/nhs-pycom/nhspy-plotthedots/pulls)
--------------------------------------------------------------------------------
/docs/_site/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | https://nhs-pycom.github.io/nhspy-plotthedots/documentation/index.html
5 | 2023-01-22T21:57:06.314Z
6 |
7 |
8 | https://nhs-pycom.github.io/nhspy-plotthedots/posts/welcome/index.html
9 | 2023-01-22T20:57:04.441Z
10 |
11 |
12 | https://nhs-pycom.github.io/nhspy-plotthedots/posts/rap-maturity/index.html
13 | 2023-01-22T20:57:05.331Z
14 |
15 |
16 | https://nhs-pycom.github.io/nhspy-plotthedots/contribute.html
17 | 2023-01-22T21:38:35.503Z
18 |
19 |
20 | https://nhs-pycom.github.io/nhspy-plotthedots/index.html
21 | 2023-01-23T17:09:00.688Z
22 |
23 |
24 | https://nhs-pycom.github.io/nhspy-plotthedots/about.html
25 | 2023-01-22T20:57:10.103Z
26 |
27 |
28 | https://nhs-pycom.github.io/nhspy-plotthedots/updates.html
29 | 2023-01-22T20:57:10.394Z
30 |
31 |
32 | https://nhs-pycom.github.io/nhspy-plotthedots/faq.html
33 | 2023-01-22T21:57:06.585Z
34 |
35 |
36 | https://nhs-pycom.github.io/nhspy-plotthedots/tutorials/index.html
37 | 2023-01-23T17:08:00.386Z
38 |
39 |
40 | https://nhs-pycom.github.io/nhspy-plotthedots/documentation/pandas_spc_x_calc.html
41 | 2023-01-22T21:57:06.054Z
42 |
43 |
44 | https://nhs-pycom.github.io/nhspy-plotthedots/tutorials/pandas.html
45 | 2023-01-22T21:57:06.831Z
46 |
47 |
48 |
--------------------------------------------------------------------------------
/tests/unittests/test_part_of_two_in_three.py:
--------------------------------------------------------------------------------
1 | # Python source
2 | # -------------------------------------------------------------------------
3 | # Copyright (c) 2023 NHS Python Community. All rights reserved.
4 | # Licensed under the MIT License. See license.txt in the project root for
5 | # license information.
6 | # -------------------------------------------------------------------------
7 |
8 | # FILE: test_part_of_two_in_three.py
9 | # DESCRIPTION: Tests for the part_of_two_in_three() function.
10 | # part_of_two_in_three(), given two boolean lists, uses zip()
11 | # to iterate over the two input lists and applies logical
12 | # AND to corresponding elements.
13 |
14 | # CONTRIBUTORS: v.Morriss
15 | # CONTACT: -
16 | # CREATED: 9 Jul 2024
17 | # VERSION: 0.0.1
18 |
19 |
20 | # Imports
21 | # -------------------------------------------------------------------------
22 | # Python:
23 | import unittest
24 |
25 | # Local
26 | from nhspy_plotthedots.pandas_spc_calculations import part_of_two_in_three
27 |
28 | # Define tests
29 | # -------------------------------------------------------------------------
30 |
31 | class PartOfTwoInThree(unittest.TestCase):
32 |
33 | def false(self):
34 | values1 = [False] * 10
35 | values2 = [False] * 10
36 | expected = [False] * 10
37 | self.assertEqual(part_of_two_in_three(values1, values2), expected)
38 |
39 | def true(self):
40 | values1 = [True] * 10
41 | values2 = [True] * 10
42 | expected = [True] * 10
43 | self.assertEqual(part_of_two_in_three(values1, values2), expected)
44 |
45 | def mixed(self):
46 | values1 = [False, True, False, True]
47 | values2 = [False, False, True, True]
48 | expected = [False, False, False, True]
49 | self.assertEqual(part_of_two_in_three(values1, values2), expected)
50 |
51 | def test_null_input(self):
52 | values1 = []
53 | values2 = []
54 | expected = []
55 | self.assertEqual(part_of_two_in_three(values1, values2), expected)
56 |
57 | def left_uneven(self):
58 | values1 = [True, True, True]
59 | values2 = [True]
60 | expected = [True]
61 | self.assertEqual(part_of_two_in_three(values1, values2), expected)
62 |
63 | def right_uneven(self):
64 | values1 = [True]
65 | values2 = [True,True,True]
66 | expected = [True]
67 | self.assertEqual(part_of_two_in_three(values1, values2), expected)
68 |
69 | if __name__ == '__main__':
70 | unittest.main()
71 |
--------------------------------------------------------------------------------
/tests/unittests/test_seven_point_one_side_mean.py:
--------------------------------------------------------------------------------
1 | # -------------------------------------------------------------------------
2 | # Copyright (c) 2023 NHS Python Community. All rights reserved.
3 | # Licensed under the MIT License. See license.txt in the project root for
4 | # license information.
5 | # -------------------------------------------------------------------------
6 |
7 | # FILE: test_seven_point_one_side_mean.py
8 |
9 | # DESCRIPTION: Tests for the seven_point_one_side_mean() function.
10 | # Given a list of floats representing each entries'
11 | # relation to the mean (1.0 = above, 0.0 = mean, -1.0 = below),
12 | # it returns a boolean list with each entry representing if a
13 | # number is the seventh number above/below the mean.
14 | #
15 | # CONTRIBUTORS: v.Morriss
16 | # CONTACT: -
17 | # CREATED: 9 Jul 2024
18 | # VERSION: 0.0.1
19 |
20 | # Imports
21 | # -------------------------------------------------------------------------
22 | # Python:
23 | import unittest
24 |
25 | # 3rd party:
26 |
27 | # Local
28 | from nhspy_plotthedots.pandas_spc_calculations import seven_point_one_side_mean
29 |
30 | # Define tests
31 | # -------------------------------------------------------------------------
32 |
33 |
34 | class TestSevenPointOneSideMean(unittest.TestCase):
35 |
36 | def test_above(self):
37 | values = [1] * 7
38 | expected = [False] * 6 + [True]
39 | self.assertEqual(seven_point_one_side_mean(values), expected)
40 |
41 | def test_below(self):
42 | values = [-1] * 7
43 | expected = [False] * 6 + [True]
44 | self.assertEqual(seven_point_one_side_mean(values), expected)
45 |
46 | def test_equal(self):
47 | values = [-1,0,1,0,-1,0,1]
48 | expected = [False] * 7
49 | self.assertEqual(seven_point_one_side_mean(values), expected)
50 |
51 | def test_zero(self):
52 | values = [0] * 7
53 | expected = [True] * 7
54 | self.assertEqual(seven_point_one_side_mean(values), expected)
55 |
56 | def test_small_input(self):
57 | values = [1, 0, -1]
58 | expected = [0, 0, 0]
59 | self.assertEqual(seven_point_one_side_mean(values), expected)
60 |
61 | def test_large_input(self):
62 | values = [1] * 10 + [-1] * 10
63 | expected = [False] * 6 + [True] * 4 + [False] * 6 + [True] * 4
64 | self.assertEqual(seven_point_one_side_mean(values), expected)
65 |
66 | def test_invalid(self):
67 | values = list(range(-10,10,1))
68 | expected = [False] * 20
69 | self.assertEqual(seven_point_one_side_mean(values), expected)
70 |
71 | def test_null(self):
72 | values = []
73 | expected = []
74 | self.assertEqual(seven_point_one_side_mean(values), expected)
75 |
76 | if __name__ == '__main__':
77 | unittest.main()
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | pull_request:
16 | branches: [ "main" ]
17 |
18 | jobs:
19 | analyze:
20 | name: Analyze
21 | runs-on: ubuntu-latest
22 | permissions:
23 | actions: read
24 | contents: read
25 | security-events: write
26 |
27 | strategy:
28 | fail-fast: false
29 | matrix:
30 | language: [ 'python' ]
31 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
32 | # Use only 'java' to analyze code written in Java, Kotlin or both
33 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
34 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
35 |
36 | steps:
37 | - name: Checkout repository
38 | uses: actions/checkout@v3
39 |
40 | # Initializes the CodeQL tools for scanning.
41 | - name: Initialize CodeQL
42 | uses: github/codeql-action/init@v2
43 | with:
44 | languages: ${{ matrix.language }}
45 | # If you wish to specify custom queries, you can do so here or in a config file.
46 | # By default, queries listed here will override any specified in a config file.
47 | # Prefix the list here with "+" to use these queries and those in the config file.
48 |
49 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
50 | # queries: security-extended,security-and-quality
51 |
52 |
53 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
54 | # If this step fails, then you should remove it and run the build manually (see below)
55 | - name: Autobuild
56 | uses: github/codeql-action/autobuild@v2
57 |
58 | # ℹ️ Command-line programs to run using the OS shell.
59 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
60 |
61 | # If the Autobuild fails above, remove it and uncomment the following three lines.
62 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
63 |
64 | # - run: |
65 | # echo "Run, Build Application using script"
66 | # ./location_of_script_within_repo/buildscript.sh
67 |
68 | - name: Perform CodeQL Analysis
69 | uses: github/codeql-action/analyze@v2
70 | with:
71 | category: "/language:${{matrix.language}}"
72 |
--------------------------------------------------------------------------------
/docs/tutorials/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Get Started"
3 | subtitle: "A quick tutorial to learn the basics."
4 | comments: false
5 | anchor-sections: false
6 | ---
7 |
8 | ::::: {.grid .step .column-page-right}
9 |
10 | :::: {.g-col-lg-2 .g-col-12}
11 | ## Step 1
12 |
13 | #### Install {.fw-light}
14 | ::::
15 |
16 | :::: {.g-col-lg-7 .g-col-12}
17 |
18 | ## Download the code locally
19 |
20 | ```{python}
21 | #| output: false
22 |
23 | # Install libs from the test Python Package Index (pypi) repo
24 | %pip install --index-url https://test.pypi.org/simple/ --no-deps nhspy-plotthedots-test
25 |
26 | # Import plotthedots to your environment
27 | from nhspy_plotthedots import pandas_spc_calculations
28 | from nhspy_plotthedots import plotly_spc_chart
29 | ```
30 |
31 | ::::
32 |
33 | :::::
34 |
35 | ::::: {.grid .step .column-page-right}
36 |
37 | :::: {.g-col-lg-2 .g-col-12}
38 | ## Step 2
39 |
40 | #### Process Data {.fw-light}
41 | ::::
42 |
43 | :::: {.g-col-lg-7 .g-col-12}
44 |
45 | ## Load time-series data into `pandas`
46 |
47 | ```{python}
48 | import pandas as pd
49 | from datetime import datetime
50 |
51 | # url path of the CSV file
52 | url = 'https://raw.githubusercontent.com/nhs-pycom/nhspy-plotthedots/main/nhspy_plotthedots/data/ae_attendances.csv'
53 |
54 | # Read the CSV file and store it in a pandas DataFrame
55 | df = pd.read_csv(url)
56 |
57 | # Convert 'period' column to datetime format
58 | df['period'] = pd.to_datetime(df['period'])
59 |
60 | # Create a subset of the DataFrame based on certain conditions
61 | sub_set = df[(df['org_code'] == "RQM") & (df['type'] == "1") & (df['period'] < datetime(2018, 4, 1))]
62 |
63 | # It's important to sort values by date for time-series and reset index for spc calculations
64 | sub_set = sub_set.sort_values(by='period').reset_index(drop=True)
65 | sub_set.head()
66 | ```
67 |
68 | ::::
69 |
70 | :::::
71 |
72 | ::::: {.grid .step .column-page-right}
73 |
74 | :::: {.g-col-lg-2 .g-col-12}
75 | ## Step 3
76 |
77 | #### Calculation {.fw-light}
78 | ::::
79 |
80 | :::: {.g-col-lg-7 .g-col-12}
81 |
82 | ## Calculate the control limits
83 | ```{python}
84 | # calculate control limits
85 | spc = pandas_spc_calculations.pandas_spc_x_calc(sub_set, 'breaches')
86 | spc.head()
87 | ```
88 |
89 | ::::
90 |
91 | :::::
92 |
93 | ::::: {.grid .step .column-page-right}
94 |
95 | :::: {.g-col-lg-2 .g-col-12}
96 | ## Step 4
97 |
98 | #### Visualisation {.fw-light}
99 | ::::
100 |
101 | :::: {.g-col-lg-7 .g-col-12}
102 |
103 | ## Plot the statistical process control chart
104 | ```{python}
105 | #| label: fig-4-hour-breach1
106 | #| fig-cap: Number of A&E attendance 4-Hour Breaches
107 | #| fig-align: center
108 |
109 | import plotly.io as pio
110 | pio.renderers.default = "plotly_mimetype+notebook"
111 |
112 | # plot SPC chart
113 | plotly_spc_chart.plotly_spc_chart(spc, 'breaches', 'period', plot_title = 'Chelsea & Westminster Hospital NHS FT', x_lab = 'Month of attendance', y_lab = 'Number of 4-Hour Target Breaches')
114 | ```
115 |
116 | ::::
117 |
118 | :::::
--------------------------------------------------------------------------------
/docs/posts/rap-maturity/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "RAP Maturity Framework"
3 | author: "Craig R Shenton"
4 | date: "2023-01-23"
5 | categories: [NHS, RAP]
6 | image: "nhsd-rap.png"
7 | description: "Lets make nhspy-plotthedots fully RAP complient"
8 | title-block-banner: false
9 | ---
10 |
11 | ## The 'Levels of RAP' Maturity Framework
12 |
13 | We are going to be developing `nhspy-plotthedots` in a Reproducible Analytical Pipeline (RAP) way by following the maturity framework developed by [NHS Digital RAP community](https://nhsdigital.github.io/rap-community-of-practice/introduction_to_RAP/levels_of_RAP/).
14 |
15 | There are three levels to RAP:
16 |
17 | 1. Baseline - **RAP fundamentals** offering resilience against future change.
18 | 2. Silver - **Implementing best practice** by following good analytical and software engineering standards.
19 | 3. Gold - **Analysis as a product** to further elevate your analytical work and enhance its reusability to the public.
20 |
21 | ## Baseline RAP - getting the fundamentals right
22 |
23 | In order for a publication to be considered a reproducible analytical pipeline, it must at least meet all of the requirements of Baseline RAP:
24 |
25 | - [x] Data produced by code in an open-source language (e.g., Python, R, SQL).
26 | - [x] Code is version controlled (i.e., Git & GitHub).
27 | - [ ] Repository includes a README.md file (or equivalent) that clearly details steps a user must follow to reproduce the code.
28 | - [ ] Code has been peer reviewed (i.e., use PRs and code reviews)
29 | - [x] Code is published in the open and linked to & from accompanying publication (if relevant).
30 |
31 | ## Silver RAP - implementing best practice
32 |
33 | _Meeting all of the above requirements, plus:_
34 |
35 | - [x] Outputs are produced by code with minimal manual intervention.
36 | - [ ] Code is well-documented including user guidance, explanation of code structure & methodology and docstrings for functions.
37 | - [x] Code is well-organised following standard directory format.
38 | - [x] Reusable functions and/or classes are used where appropriate.
39 | - [ ] Code adheres to agreed coding standards (e.g., PEP8).
40 | - [ ] Pipeline includes a testing framework (unit tests, back tests).
41 | - [x] Repository includes package dependency information.
42 | - [ ] Logs are automatically recorded by the pipeline to ensure outputs are as expected.
43 | - [ ] Data is handled and output in a [Tidy data format](https://medium.com/@kimrodrikwa/untidy-data-a90b6e3ebe4c).
44 |
45 | ## Gold RAP - analysis as a product
46 |
47 | _Meeting all of the above requirements, plus:_
48 |
49 | - [x] Code is fully packaged.
50 | - [x] Repository automatically runs tests etc. via [CI](https://github.com/skills/continuous-integration)/CD or a different integration/deployment tool e.g. [GitHub Actions](https://docs.github.com/en/actions).
51 | - [ ] Process runs based on event-based triggers (e.g., new data in database) or on a schedule.
52 | - [ ] Changes to the RAP are clearly signposted. E.g. a changelog in the package, releases etc. (See gov.uk info on [Semantic Versioning](https://github.com/alphagov/govuk-frontend/blob/main/docs/contributing/versioning.md))
53 |
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ####################################################################
2 | # GENERIC GITIGNORE FILE FOR PYTHON
3 | # Copied from https://github.com/github/gitignore/blob/master/Python.gitignore
4 | # This is a useful thing to just copy and paste since it should work for most use cases.
5 | # If you have additional things to exclude from the .gitignore file you should add it above
6 | # this section.
7 | #####################################################################
8 |
9 | # Mac OS
10 | .DS_Store
11 |
12 | # Byte-compiled / optimized / DLL files
13 | __pycache__/
14 | *.py[cod]
15 | *$py.class
16 |
17 | # C extensions
18 | *.so
19 |
20 | # Distribution / packaging
21 | .Python
22 | build/
23 | develop-eggs/
24 | dist/
25 | downloads/
26 | eggs/
27 | .eggs/
28 | lib/
29 | lib64/
30 | parts/
31 | sdist/
32 | var/
33 | wheels/
34 | share/python-wheels/
35 | *.egg-info/
36 | .installed.cfg
37 | *.egg
38 | MANIFEST
39 |
40 | # PyInstaller
41 | # Usually these files are written by a python script from a template
42 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
43 | *.manifest
44 | *.spec
45 |
46 | # Installer logs
47 | pip-log.txt
48 | pip-delete-this-directory.txt
49 |
50 | # Unit test / coverage reports
51 | htmlcov/
52 | .tox/
53 | .nox/
54 | .coverage
55 | .coverage.*
56 | .cache
57 | nosetests.xml
58 | coverage.xml
59 | *.cover
60 | *.py,cover
61 | .hypothesis/
62 | .pytest_cache/
63 | cover/
64 |
65 | # Translations
66 | *.mo
67 | *.pot
68 |
69 | # Django stuff:
70 | *.log
71 | local_settings.py
72 | db.sqlite3
73 | db.sqlite3-journal
74 |
75 | # Flask stuff:
76 | instance/
77 | .webassets-cache
78 |
79 | # Scrapy stuff:
80 | .scrapy
81 |
82 | # Sphinx documentation
83 | docs/_build/
84 |
85 | # PyBuilder
86 | .pybuilder/
87 | target/
88 |
89 | # Jupyter Notebook
90 | .ipynb_checkpoints
91 |
92 | # IPython
93 | profile_default/
94 | ipython_config.py
95 |
96 | # pyenv
97 | # For a library or package, you might want to ignore these files since the code is
98 | # intended to run in multiple environments; otherwise, check them in:
99 | # .python-version
100 |
101 | # pipenv
102 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
103 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
104 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
105 | # install all needed dependencies.
106 | #Pipfile.lock
107 |
108 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
109 | __pypackages__/
110 |
111 | # Celery stuff
112 | celerybeat-schedule
113 | celerybeat.pid
114 |
115 | # SageMath parsed files
116 | *.sage.py
117 |
118 | # Environments
119 | .env
120 | .venv
121 | env/
122 | venv/
123 | ENV/
124 | env.bak/
125 | venv.bak/
126 |
127 | # Spyder project settings
128 | .spyderproject
129 | .spyproject
130 |
131 | # Rope project settings
132 | .ropeproject
133 |
134 | # mkdocs documentation
135 | /site
136 |
137 | # mypy
138 | .mypy_cache/
139 | .dmypy.json
140 | dmypy.json
141 |
142 | # Pyre type checker
143 | .pyre/
144 |
145 | # pytype static type analyzer
146 | .pytype/
147 |
148 | # Cython debug symbols
149 | cython_debug/
--------------------------------------------------------------------------------
/tests/unittests/test_two_in_three.py:
--------------------------------------------------------------------------------
1 | # Python source
2 | # -------------------------------------------------------------------------
3 | # Copyright (c) 2023 NHS Python Community. All rights reserved.
4 | # Licensed under the MIT License. See license.txt in the project root for
5 | # license information.
6 | # -------------------------------------------------------------------------
7 |
8 | # FILE: test_two_in_three.py
9 | # DESCRIPTION: Tests for the two_in_three() function.
10 | # two_in_three() checks if there is a trend of 3 elements
11 | # where at least 2 elements are close to limits and sum of
12 | # relative to mean is 3.
13 |
14 | # CONTRIBUTORS: v.Morriss
15 | # CONTACT: -
16 | # CREATED: 9 Jul 2024
17 | # VERSION: 0.0.1
18 |
19 | # Imports
20 | # -------------------------------------------------------------------------
21 | # Python:
22 | import unittest
23 |
24 | # Local
25 | from nhspy_plotthedots.pandas_spc_calculations import two_in_three
26 |
27 | # Define tests
28 | # -------------------------------------------------------------------------
29 | class TestTwoInThree(unittest.TestCase):
30 |
31 | def test_large(self):
32 | bool_values = [True] * 81
33 | values = [
34 | -1,-1,-1,-1,-1,0,-1,-1,1,-1,0,-1,-1,0,0,-1,0,1,-1,1,-1,-1,1,0,-1,1,1,
35 | 0,-1,-1,0,-1,0,0,-1,1,0,0,-1,0,0,0,0,0,1,0,1,-1,0,1,0,0,1,1,
36 | 1,-1,-1,1,-1,0,1,-1,1,1,0,-1,1,0,0,1,0,1,1,1,-1,1,1,0,1,1,1
37 | ]
38 | expected = [True] * 5 + [False] * 47 + [True] * 3 + [False] * 16 + [True] * 3 + [False] * 4 + [True] * 3
39 | self.assertEqual(two_in_three(bool_values, values), expected)
40 |
41 | def test_negative(self):
42 | bool_values = [True] * 3
43 | values = [-1,-1,-1]
44 | expected = [True] * 3
45 | self.assertEqual(two_in_three(bool_values, values), expected)
46 |
47 | def test_relative_to_mean(self):
48 | bool_values = [True] * 3
49 | values = [1,-1,1]
50 | expected = [False] * 3
51 | self.assertEqual(two_in_three(bool_values, values), expected)
52 |
53 | def test_close_to_limits(self):
54 | bool_values = [False] * 3
55 | values = [1] * 3
56 | expected = [False] * 3
57 | self.assertEqual(two_in_three(bool_values, values), expected)
58 |
59 | def test_unequal_sizes(self):
60 | bool_values = [True,False,True,False]
61 | values = [1,-1]
62 | expected = [False] * 4
63 | self.assertEqual(two_in_three(bool_values, values), expected)
64 |
65 | def test_null_boolean_input(self):
66 | bool_values = []
67 | values = [1,1,1]
68 | expected = []
69 | self.assertEqual(two_in_three(bool_values, values), expected)
70 |
71 | def test_null_value_input(self):
72 | bool_values = [True, False]
73 | values = []
74 | expected = [False, False]
75 | self.assertEqual(two_in_three(bool_values, values), expected)
76 |
77 | def test_null_input(self):
78 | bool_values = []
79 | values = []
80 | expected = []
81 | self.assertEqual(two_in_three(bool_values, values), expected)
82 |
83 |
84 | if __name__ == '__main__':
85 | unittest.main()
--------------------------------------------------------------------------------
/docs/_site/site_libs/quarto-html/quarto-syntax-highlighting.css:
--------------------------------------------------------------------------------
1 | /* quarto syntax highlight colors */
2 | :root {
3 | --quarto-hl-ot-color: #003B4F;
4 | --quarto-hl-at-color: #657422;
5 | --quarto-hl-ss-color: #20794D;
6 | --quarto-hl-an-color: #5E5E5E;
7 | --quarto-hl-fu-color: #4758AB;
8 | --quarto-hl-st-color: #20794D;
9 | --quarto-hl-cf-color: #003B4F;
10 | --quarto-hl-op-color: #5E5E5E;
11 | --quarto-hl-er-color: #AD0000;
12 | --quarto-hl-bn-color: #AD0000;
13 | --quarto-hl-al-color: #AD0000;
14 | --quarto-hl-va-color: #111111;
15 | --quarto-hl-bu-color: inherit;
16 | --quarto-hl-ex-color: inherit;
17 | --quarto-hl-pp-color: #AD0000;
18 | --quarto-hl-in-color: #5E5E5E;
19 | --quarto-hl-vs-color: #20794D;
20 | --quarto-hl-wa-color: #5E5E5E;
21 | --quarto-hl-do-color: #5E5E5E;
22 | --quarto-hl-im-color: #00769E;
23 | --quarto-hl-ch-color: #20794D;
24 | --quarto-hl-dt-color: #AD0000;
25 | --quarto-hl-fl-color: #AD0000;
26 | --quarto-hl-co-color: #5E5E5E;
27 | --quarto-hl-cv-color: #5E5E5E;
28 | --quarto-hl-cn-color: #8f5902;
29 | --quarto-hl-sc-color: #5E5E5E;
30 | --quarto-hl-dv-color: #AD0000;
31 | --quarto-hl-kw-color: #003B4F;
32 | }
33 |
34 | /* other quarto variables */
35 | :root {
36 | --quarto-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
37 | }
38 |
39 | pre > code.sourceCode > span {
40 | color: #003B4F;
41 | }
42 |
43 | code span {
44 | color: #003B4F;
45 | }
46 |
47 | code.sourceCode > span {
48 | color: #003B4F;
49 | }
50 |
51 | div.sourceCode,
52 | div.sourceCode pre.sourceCode {
53 | color: #003B4F;
54 | }
55 |
56 | code span.ot {
57 | color: #003B4F;
58 | }
59 |
60 | code span.at {
61 | color: #657422;
62 | }
63 |
64 | code span.ss {
65 | color: #20794D;
66 | }
67 |
68 | code span.an {
69 | color: #5E5E5E;
70 | }
71 |
72 | code span.fu {
73 | color: #4758AB;
74 | }
75 |
76 | code span.st {
77 | color: #20794D;
78 | }
79 |
80 | code span.cf {
81 | color: #003B4F;
82 | }
83 |
84 | code span.op {
85 | color: #5E5E5E;
86 | }
87 |
88 | code span.er {
89 | color: #AD0000;
90 | }
91 |
92 | code span.bn {
93 | color: #AD0000;
94 | }
95 |
96 | code span.al {
97 | color: #AD0000;
98 | }
99 |
100 | code span.va {
101 | color: #111111;
102 | }
103 |
104 | code span.pp {
105 | color: #AD0000;
106 | }
107 |
108 | code span.in {
109 | color: #5E5E5E;
110 | }
111 |
112 | code span.vs {
113 | color: #20794D;
114 | }
115 |
116 | code span.wa {
117 | color: #5E5E5E;
118 | font-style: italic;
119 | }
120 |
121 | code span.do {
122 | color: #5E5E5E;
123 | font-style: italic;
124 | }
125 |
126 | code span.im {
127 | color: #00769E;
128 | }
129 |
130 | code span.ch {
131 | color: #20794D;
132 | }
133 |
134 | code span.dt {
135 | color: #AD0000;
136 | }
137 |
138 | code span.fl {
139 | color: #AD0000;
140 | }
141 |
142 | code span.co {
143 | color: #5E5E5E;
144 | }
145 |
146 | code span.cv {
147 | color: #5E5E5E;
148 | font-style: italic;
149 | }
150 |
151 | code span.cn {
152 | color: #8f5902;
153 | }
154 |
155 | code span.sc {
156 | color: #5E5E5E;
157 | }
158 |
159 | code span.dv {
160 | color: #AD0000;
161 | }
162 |
163 | code span.kw {
164 | color: #003B4F;
165 | }
166 |
167 | .prevent-inlining {
168 | content: "";
169 | }
170 |
171 | /*# sourceMappingURL=debc5d5d77c3f9108843748ff7464032.css.map */
172 |
--------------------------------------------------------------------------------
/tests/unittests/test_seven_point_trend.py:
--------------------------------------------------------------------------------
1 | # Python source
2 | # -------------------------------------------------------------------------
3 | # Copyright (c) 2023 NHS Python Community. All rights reserved.
4 | # Licensed under the MIT License. See license.txt in the project root for
5 | # license information.
6 | # -------------------------------------------------------------------------
7 |
8 | # FILE: test_seven_point_trend.py
9 |
10 | # DESCRIPTION: Tests on the seven_point_trend() function. Given a list of floats,
11 | # this function checks for a trend in the last 7 values. It returns
12 | # a list of integers indicating the trend (-1 for decreasing trend,
13 | # 1 for increasing trend, and 0 for no trend)
14 | #
15 | # These tests cover different scenarios such as an increasing trend,
16 | # a decreasing trend, no trend, small input, large input, exactly 7
17 | # values, no input, mixed input, and negative input.
18 |
19 | # CONTRIBUTORS: Craig R. Shenton
20 | # CONTACT: craig.shenton@nhs.net
21 | # CREATED: 21 Jan 2023
22 | # VERSION: 0.0.1
23 |
24 | # Imports
25 | # -------------------------------------------------------------------------
26 | # Python:
27 | import unittest
28 |
29 | # Local
30 | from nhspy_plotthedots.pandas_spc_calculations import seven_point_trend
31 |
32 | # Define tests
33 | # -------------------------------------------------------------------------
34 | class TestSevenPointTrend(unittest.TestCase):
35 |
36 | def test_increasing_trend(self):
37 | values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
38 | expected = [0, 0, 0, 0, 0, 0, 1, 1, 1, 1]
39 | self.assertEqual(seven_point_trend(values), expected)
40 |
41 | def test_decreasing_trend(self):
42 | values = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
43 | expected = [0, 0, 0, 0, 0, 0, -1, -1, -1, -1]
44 | self.assertEqual(seven_point_trend(values), expected)
45 |
46 | def test_no_trend(self):
47 | values = [1, 2, 3, 4, 5, 4, 3, 2, 1, 2]
48 | expected = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
49 | self.assertEqual(seven_point_trend(values), expected)
50 |
51 | def test_small_input(self):
52 | values = [1, 2, 3]
53 | expected = [0, 0, 0]
54 | self.assertEqual(seven_point_trend(values), expected)
55 |
56 | def test_large_input(self):
57 | values = list(range(20))
58 | expected = [0, 0, 0, 0, 0, 0] + [1] * 14
59 | self.assertEqual(seven_point_trend(values), expected)
60 |
61 | def test_exact_seven_input(self):
62 | values = [1, 1, 1, 1, 1, 1, 1]
63 | expected = [0, 0, 0, 0, 0, 0, 0]
64 | self.assertEqual(seven_point_trend(values), expected)
65 |
66 | def test_null_input(self):
67 | values = []
68 | expected = []
69 | self.assertEqual(seven_point_trend(values), expected)
70 |
71 | def test_mixed_input(self):
72 | values = [1, 2, 3, 2, 1, 2, 3, 4, 5, 6, 7, 4, 3, 2, 1, 2]
73 | expected = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]
74 | self.assertEqual(seven_point_trend(values), expected)
75 |
76 | def test_negative_input(self):
77 | values = [-1, -2, -3, -2, -1, -2, -3, -4, -5, -6, -7, -4, -3, -2, -1, -2]
78 | expected = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0]
79 | self.assertEqual(seven_point_trend(values), expected)
80 |
81 | if __name__ == '__main__':
82 | unittest.main()
83 |
--------------------------------------------------------------------------------
/docs/_quarto.yml:
--------------------------------------------------------------------------------
1 | project:
2 | type: website
3 | # resources:
4 | # - "images/twitter-card.png"
5 | # - "course-materials/_slides/"
6 |
7 | website:
8 | open-graph: true
9 | page-navigation: true
10 | title: "nhspy-plotthedots"
11 | description: "Draw XmR charts in python for NHSE 'Making Data Count' programme."
12 | # date: now
13 | favicon: _assets/favicons/favicon.ico
14 | repo-url: https://github.com/nhs-pycom/nhspy-plotthedots
15 | repo-actions: [edit, issue]
16 | site-url: https://nhs-pycom.github.io/nhspy-plotthedots
17 |
18 | page-footer:
19 | left: "This page is built [Quarto](https://quarto.org/)."
20 | # background: "#005eb8"
21 | right:
22 | - text: "License"
23 | href: https://github.com/nhs-pycom/nhspy-plotthedots/blob/main/LICENSE
24 | - text: "Code of Conduct"
25 | href: https://github.com/nhs-pycom/nhspy-plotthedots/blob/main/CODE_OF_CONDUCT.md
26 |
27 | navbar:
28 | background: light
29 | title: false
30 | collapse-below: lg
31 | left:
32 | - text: "nhspy-plotthedots"
33 | icon: "box-seam"
34 | href: index.qmd
35 | - text: "Tutorials"
36 | icon: "bookmark-check"
37 | href: tutorials/index.qmd
38 | - text: "Documentation"
39 | icon: "file-earmark-text"
40 | href: documentation/index.qmd
41 | - text: "Updates"
42 | icon: "rss"
43 | href: updates.qmd
44 | right:
45 | - text: "Help"
46 | menu:
47 | - text: "About"
48 | icon: "info-circle"
49 | href: about.qmd
50 | - text: "Report a Bug"
51 | icon: "bug"
52 | href: "https://github.com/nhs-pycom/nhspy-plotthedots/issues"
53 | - text: "Ask a Question"
54 | icon: "chat-right-text"
55 | href: "https://github.com/nhs-pycom/nhspy-plotthedots/discussions"
56 | - text: "FAQ"
57 | icon: "question-circle"
58 | href: faq.qmd
59 | - icon: github
60 | href: https://github.com/nhs-pycom/
61 | aria-label: GitHub
62 | - icon: cloud-fill
63 | href: https://nhs-pycom.net/
64 | aria-label: NHS Python Community
65 | sidebar:
66 | id: toc-side
67 | style: "floating"
68 | pinned: true
69 | contents:
70 | - text: "Homepage"
71 | file: index.qmd
72 | - text: "Contribute to docs"
73 | file: contribute.qmd
74 | - section: Tutorials
75 | file: tutorials/index.qmd
76 | contents:
77 | - auto: tutorials/*.qmd
78 | - section: Documentation
79 | file: documentation/index.qmd
80 | contents:
81 | - auto: documentation/*.qmd
82 | - text: "FAQ"
83 | file: faq.qmd
84 | - text: "About"
85 | file: about.qmd
86 | comments:
87 | giscus:
88 | repo: nhs-pycom/nhspy-plotthedots
89 | format:
90 | html:
91 | toc: true
92 | toc-depth: 4
93 | theme:
94 | light: [cosmo, _assets/style/theme.scss]
95 | code-copy: true
96 | code-overflow: wrap
97 | css: _assets/style/styles.css
98 | include-after-body: _assets/style/js.html
99 | # grid:
100 | # sidebar-width: 250px
101 | # body-width: 900px
102 | # margin-width: 300px
103 | # # Code options
104 | # code-tools:
105 | # source: false
106 | # toggle: false
107 | # caption: none
108 | # code-fold: true
109 | # code-summary: "Show code"
110 | # code-copy: true
111 | # code-overflow: wrap
112 | # - icon: box-seam
113 | # href: https://cloud.r-project.org/web/packages/
114 |
115 | execute:
116 | freeze: auto
--------------------------------------------------------------------------------
/tests/unittests/test_limits_calculations.py:
--------------------------------------------------------------------------------
1 | # Python source
2 | # -------------------------------------------------------------------------
3 | # Copyright (c) 2023 NHS Python Community. All rights reserved.
4 | # Licensed under the MIT License. See license.txt in the project root for
5 | # license information.
6 | # -------------------------------------------------------------------------
7 |
8 | # FILE: test_limits_calculations.py
9 | # DESCRIPTION: Tests for the limits_calculations() function.
10 | # Given a list of floats (fix_values) it returns a tuple of floats:
11 | # - mean: The mean of the input values
12 | # - lpl: The lower process limit of the input values
13 | # - upl: The upper process limit of the input values
14 | # - nlpl: The near lower process limit of the input values
15 | # - nupl: The near upper process limit of the input values
16 |
17 | # CONTRIBUTORS: v.Morriss
18 | # CONTACT: -
19 | # CREATED: 9 Jul 2024
20 | # VERSION: 0.0.1
21 |
22 | # Imports
23 | # -------------------------------------------------------------------------
24 | # Python:
25 | import unittest
26 |
27 | # 3rd Party:
28 | import numpy as np
29 |
30 | # Local
31 | from nhspy_plotthedots.pandas_spc_calculations import limits_calculations
32 | # Define tests
33 | # -------------------------------------------------------------------------
34 | class LimitsCalculations(unittest.TestCase):
35 | def test_increasing_trend(self):
36 | values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
37 | expected = (5.5, 2.84, 8.16, 3.7266666666666666, 7.273333333333333)
38 | self.assertEqual(limits_calculations(values), expected)
39 |
40 | def test_decreasing_trend(self):
41 | values = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
42 | expected = (5.5, 2.84, 8.16, 3.7266666666666666, 7.273333333333333)
43 | self.assertEqual(limits_calculations(values), expected)
44 |
45 | def test_no_trend(self):
46 | values = [1, 2, 3, 4, 5, 4, 3, 2, 1, 2]
47 | expected = (2.7, 0.040000000000000036, 5.36, 0.9266666666666667, 4.473333333333334)
48 | self.assertEqual(limits_calculations(values), expected)
49 |
50 | def test_small_input(self):
51 | values = [1, 2, 3]
52 | expected = (2.0, -0.6600000000000001, 4.66, 0.22666666666666657, 3.7733333333333334)
53 | self.assertEqual(limits_calculations(values), expected)
54 |
55 | def test_large_input(self):
56 | values = list(range(20))
57 | expected = (9.5, 6.84, 12.16, 7.726666666666667, 11.273333333333333)
58 | self.assertEqual(limits_calculations(values), expected)
59 |
60 | def test_exact_seven_input(self):
61 | values = [3, 1, 4, 1, 5, 9, 2]
62 | expected = (3.5714285714285716, -6.625238095238096, 13.768095238095238, -3.2263492063492065, 10.36920634920635)
63 | self.assertEqual(limits_calculations(values), expected)
64 |
65 | def test_null_input(self):
66 | values = []
67 | np.isnan(limits_calculations(values)).all()
68 |
69 | def test_mixed_input(self):
70 | values = [1, 2, 3, 2, 1, 2, 3, 4, 5, 6, 7, 4, 3, 2, 1, 2]
71 | expected = (3.0, -0.014666666666666828, 6.014666666666667, 0.9902222222222221, 5.009777777777778)
72 | self.assertEqual(limits_calculations(values), expected)
73 |
74 | def test_negative_input(self):
75 | values = [-1, -2, -3, -2, -1, -2, -3, -4, -5, -6, -7, -4, -3, -2, -1, -2]
76 | expected = (-3.0, -6.014666666666667, 0.014666666666666828, -5.009777777777778, -0.9902222222222221)
77 | self.assertEqual(limits_calculations(values), expected)
78 |
79 | if __name__ == '__main__':
80 | unittest.main()
81 |
--------------------------------------------------------------------------------
/tests/unittests/test_part_of_seven_trend.py:
--------------------------------------------------------------------------------
1 | # Python source
2 | # -------------------------------------------------------------------------
3 | # Copyright (c) 2023 NHS Python Community. All rights reserved.
4 | # Licensed under the MIT License. See license.txt in the project root for
5 | # license information.
6 | # -------------------------------------------------------------------------
7 |
8 | # FILE: test_part_of_seven_trend.py
9 |
10 | # DESCRIPTION: Tests on the part_of_seven_trend() function. Checks if there
11 | # is a trend of 7 elements where at least one element has an absolute value of 1.
12 | # It returns a A list of boolean values representing whether there is
13 | # a trend of 7 elements where at least one element has an absolute
14 | # value of 1.
15 | #
16 | # These tests cover different scenarios such as an small input, large input,
17 | # exactly 7 values, no input, mixed input (asceding and descending), and negacleative input.
18 |
19 | # CONTRIBUTORS: Joan Ponsa, Craig R. Shenton
20 | # CONTACT: craig.shenton@nhs.net
21 | # CREATED: 21 Jan 2023
22 | # VERSION: 0.0.1
23 |
24 | # Imports
25 | # -------------------------------------------------------------------------
26 | # Python:
27 | import unittest
28 |
29 | # Local
30 | from nhspy_plotthedots.pandas_spc_calculations import part_of_seven_trend
31 |
32 |
33 | # Define tests
34 | # -------------------------------------------------------------------------
35 | class TestSevenPointTrend(unittest.TestCase):
36 | def test_small_input(self):
37 | values = [0, 1, 2, 3]
38 | expected = [True, True, False, False]
39 | self.assertEqual(part_of_seven_trend(values), expected)
40 |
41 | def test_large_input(self):
42 | values = list(range(20))
43 | expected = [True, True] + [False] * 18
44 | self.assertEqual(part_of_seven_trend(values), expected)
45 |
46 | def test_exact_seven_input_pos(self):
47 | values = [1, 1, 1, 1, 1, 1, 1]
48 | expected = [True] * 7
49 | self.assertEqual(part_of_seven_trend(values), expected)
50 |
51 | def test_exact_seven_input_neg(self):
52 | values = [-1, -1, -1, -1, -1, -1, -1]
53 | expected = [True] * 7
54 | self.assertEqual(part_of_seven_trend(values), expected)
55 |
56 | def test_null_input(self):
57 | values = []
58 | expected = []
59 | self.assertEqual(part_of_seven_trend(values), expected)
60 |
61 | def test_mixed_input_asc(self):
62 | values = [-2, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
63 | expected = [True, True, True] + [False] * 9
64 | self.assertEqual(part_of_seven_trend(values), expected)
65 |
66 | def test_mixed_input_des(self):
67 | values = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, -2]
68 | expected = [False] * 3 + [True] * 7 + [False] * 2
69 | self.assertEqual(part_of_seven_trend(values), expected)
70 |
71 | def test_negative_input(self):
72 | values = [-1, -2, -3, -2, -1, -2, -3, -4, -5, -6, -7, -4, -3, -2, -1, -2]
73 | expected = [True] * 5 + [False] * 3 + [True] * 7 + [False]
74 | self.assertEqual(part_of_seven_trend(values), expected)
75 |
76 | def test_perfect_pos(self):
77 | values = [1, 1, 1, 1, 1, 1, 1]
78 | expected = [True] * 7
79 | self.assertEqual(part_of_seven_trend(values), expected)
80 |
81 | def test_perfect_neg(self):
82 | values = [-1, -1, -1, -1, -1, -1, -1]
83 | expected = [True] * 7
84 | self.assertEqual(part_of_seven_trend(values), expected)
85 |
86 | def test_no_one(self):
87 | values = list(range(2,12,2))
88 | expected = [False] * 5
89 | self.assertEqual(part_of_seven_trend(values), expected)
90 |
91 | def test_parabola(self):
92 | values = [0,1,6,9,10,9,6,1,0]
93 | expected = [True] * 8 + [False]
94 | self.assertEqual(part_of_seven_trend(values), expected)
95 |
96 | if __name__ == "__main__":
97 | unittest.main()
98 |
99 |
--------------------------------------------------------------------------------
/nhspy_plotthedots/spc_calculations.py:
--------------------------------------------------------------------------------
1 | # Python source
2 | # -------------------------------------------------------------------------
3 | # Copyright (c) 2023 NHS Python Community. All rights reserved.
4 | # Licensed under the MIT License. See license.txt in the project root for
5 | # license information.
6 | # -------------------------------------------------------------------------
7 |
8 | # FILE: spc_calculations.py
9 |
10 | # DESCRIPTION: Calculations for spc_x_calc() function.
11 |
12 | # SOURCE https://gist.github.com/tomjemmett/c167376e5b6464ec1c00975be2d7864e
13 | # CONTRIBUTORS: Craig R. Shenton
14 | # CONTACT: craig.shenton@nhs.net
15 | # CREATED: 21 Jan 2023
16 | # VERSION: 0.0.1
17 |
18 | # Imports
19 | # -------------------------------------------------------------------------
20 | # Python:
21 | from typing import List, Optional, namedtuple
22 |
23 | # 3rd party:
24 | import numpy as np
25 |
26 | # Local
27 | from nhspy_plotthedots.utilities import special_cause_flag
28 |
29 | # Define seven_point_one_side_mean()
30 | # -------------------------------------------------------------------------
31 | def spc_x_calc(values: List[float], fix_after_n_points: Optional[int] = None) -> namedtuple:
32 | """
33 | Calculates the SPC for a given list of values.
34 |
35 | Parameters:
36 | - values (List[float]): The list of values for which SPC needs to be
37 | calculated.
38 | - fix_after_n_points (Optional[int]): The number of values after which
39 | the mean and other calculations should be fixed.
40 |
41 | Returns:
42 | namedtuple: A named tuple containing the following values:
43 | - values: The input values
44 | - mean: The mean of the input values
45 | - lpl: The lower process limit of the input values
46 | - upl: The upper process limit of the input values
47 | - outside_limits: A boolean list representing whether a value is
48 | outside the process limits
49 | - relative_to_mean: A list representing the relative value of an
50 | element to mean
51 | - close_to_limits: A boolean list representing whether a value is
52 | close to a limit or not
53 | - special_cause_flag: A boolean list representing whether a value
54 | is a special cause or not
55 | """
56 | # If fix_after_n_points is provided, fix the mean and other calculations
57 | # to the first n points
58 | fix_values = values[:fix_after_n_points] if fix_after_n_points else values
59 | # constant limit value
60 | limit = 2.66
61 |
62 | mean = np.mean(fix_values)
63 | mr = np.abs(np.diff(fix_values))
64 | amr = np.mean(mr)
65 |
66 | # screen for outliers
67 | mr = mr[mr < 3.267 * amr]
68 | amr = np.mean(mr)
69 |
70 | lpl = mean - (limit * amr)
71 | upl = mean + (limit * amr)
72 |
73 | # identify near lower/upper process limits
74 | nlpl = mean - (limit * 2 / 3 * amr)
75 | nupl = mean + (limit * 2 / 3 * amr)
76 |
77 | # identify any points which are outside the upper or lower process limits
78 | outside_limits = (values < lpl) | (values > upl)
79 | # identify whether a point is above or below the mean
80 | relative_to_mean = np.sign(values - mean)
81 |
82 | # identify if a point is between the near process limits and process limits
83 | close_to_limits = ~outside_limits & ((values < nlpl) | (values > nupl))
84 |
85 | spc_return_type = namedtuple("spc_x", [
86 | "values",
87 | "mean",
88 | "lpl",
89 | "upl",
90 | "outside_limits",
91 | "relative_to_mean",
92 | "close_to_limits",
93 | "special_cause_flag"
94 | ])
95 |
96 | return spc_return_type(
97 | values,
98 | mean,
99 | lpl,
100 | upl,
101 | outside_limits,
102 | relative_to_mean,
103 | close_to_limits,
104 | special_cause_flag(values, outside_limits, close_to_limits, relative_to_mean)
105 | )
--------------------------------------------------------------------------------
/tests/unittests/test_special_cause_flag.py:
--------------------------------------------------------------------------------
1 | # Python source
2 | # -------------------------------------------------------------------------
3 | # Copyright (c) 2023 NHS Python Community. All rights reserved.
4 | # Licensed under the MIT License. See license.txt in the project root for
5 | # license information.
6 | # -------------------------------------------------------------------------
7 |
8 | # FILE: test_special_cause_flag.py
9 |
10 | # DESCRIPTION: Tests for the special_cause_flag() function.
11 | # special_cause_flag() checks if an element is a special
12 | # cause based on 4 conditions:
13 | # 1. It is outside the limits
14 | # 2. It is part of a trend of 7 elements where at least one element has an
15 | # absolute value of 1
16 | # 3. It is part of a trend of 7 elements where the sum of the relative value
17 | # of elements to mean is 1
18 | # 4. It is part of a trend of 3 elements where at least 2 elements are close
19 | # to limits and sum of relative to mean is 3
20 |
21 | # Parameters:
22 | # - values (List[float]): List of float numbers
23 | # - outside_limits (List[bool]): List of boolean values representing whether
24 | # an element is outside the limits or not
25 | # - close_to_limits (List[bool]): List of boolean values representing whether
26 | # an element is close to a limit or not
27 | # - relative_to_mean (List[float]): List of float numbers representing the
28 | # relative value of an element to mean
29 |
30 | # CONTRIBUTORS: v-Morriss
31 | # CONTACT: -
32 | # CREATED: 9 Jul 2023
33 | # VERSION: 0.0.1
34 |
35 | # Imports
36 | # -------------------------------------------------------------------------
37 | # Python:
38 | import unittest
39 |
40 | # 3rd party:
41 | import numpy as np
42 |
43 | # Local
44 | from nhspy_plotthedots.pandas_spc_calculations import special_cause_flag
45 |
46 | # Define tests
47 | # -------------------------------------------------------------------------
48 | class SpecialCaseFlag(unittest.TestCase):
49 |
50 | def test_outside_limits(self):
51 | values = np.array([0,0,0])
52 | outside_limits = np.array([True, True, True]) # <- testing
53 | close_to_limits = np.array([False, False, False])
54 | relative_to_mean = np.array([0,0,0])
55 |
56 | expected = np.array([True, True, True])
57 | answer = special_cause_flag(values, outside_limits, close_to_limits, relative_to_mean)
58 | self.assertTrue((answer == expected).all())
59 |
60 | def test_sevent_point_mean(self):
61 | values = np.array([0] * 7)
62 | outside_limits = np.array([False] * 7)
63 | close_to_limits = np.array([False] * 7)
64 | relative_to_mean = np.array([1] * 7) # <- testing
65 |
66 | expected = np.array([True] * 7)
67 | answer = special_cause_flag(values, outside_limits, close_to_limits, relative_to_mean)
68 | self.assertTrue((answer == expected).all())
69 |
70 | def test_values(self):
71 | values = np.array([1] * 7) # <- testing
72 | outside_limits = np.array([False] * 7)
73 | close_to_limits = np.array([False] * 7)
74 | relative_to_mean = np.array([0] * 7)
75 |
76 | expected = np.array([True] * 7)
77 | answer = special_cause_flag(values, outside_limits, close_to_limits, relative_to_mean)
78 | self.assertTrue((answer == expected).all())
79 |
80 | def test_part_of_three(self):
81 | values = np.array([0] * 3)
82 | outside_limits = np.array([False] * 3)
83 | close_to_limits = np.array([True, True, True]) # <- testing
84 | relative_to_mean = np.array([1,1,1]) # <- testing
85 | expected = np.array([True] * 3)
86 |
87 | answer = special_cause_flag(values, outside_limits, close_to_limits, relative_to_mean)
88 | self.assertTrue((answer == expected).all())
89 |
90 | if __name__ == '__main__':
91 | unittest.main()
92 |
--------------------------------------------------------------------------------
/docs/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | pagetitle: "nhspy-plotthedots"
3 | css: index.css
4 | editor: source
5 | description: |
6 | Calculate control limits and draw statistical process control (SPC) charts in python.
7 | comments: false
8 | ---
9 |
10 | ```{python}
11 | #| echo: false
12 | #| output: false
13 | # Install libs from test PIPY
14 | %pip install --index-url https://test.pypi.org/simple/ --no-deps nhspy-plotthedots-test
15 |
16 | # Import libs
17 | import pandas as pd
18 | from datetime import datetime
19 | import plotly.io as pio
20 | pio.renderers.default = "plotly_mimetype+notebook"
21 | ```
22 |
23 | ```{python}
24 | #| echo: false
25 | #| output: false
26 | # File path of the CSV file
27 | url = 'https://raw.githubusercontent.com/nhs-pycom/nhspy-plotthedots/main/nhspy_plotthedots/data/ae_attendances.csv'
28 |
29 | # Read the CSV file and store it in a DataFrame
30 | df = pd.read_csv(url)
31 |
32 | # Convert 'period' column to datetime format
33 | df['period'] = pd.to_datetime(df['period'])
34 |
35 | # Create a subset of the DataFrame based on certain conditions
36 | sub_set = df[(df['org_code'] == "RQM") & (df['type'] == "1") & (df['period'] < datetime(2018, 4, 1))]
37 | sub_set = sub_set.sort_values(by='period').reset_index(drop=True)
38 | ```
39 |
40 | ```{python}
41 | #| echo: false
42 | #| output: false
43 | from nhspy_plotthedots import pandas_spc_calculations
44 |
45 | # calculate control limits
46 | spc = pandas_spc_calculations.pandas_spc_x_calc(sub_set, 'breaches')
47 | ```
48 |
49 | :::: {.hero-banner}
50 |
51 | ::: {.hero-image .hero-image-left}
52 | :::
53 |
54 | ::: {.content-block}
55 |
56 | # nhspy-plotthedots v0.1.6 (alpha)
57 |
58 | ### Calculate control limits and draw Statistical Process Control charts in python.
59 |
60 | ::: {.hero-buttons}
61 | [Quick Start](tutorials/index.qmd){.btn-action-primary .btn-action .btn .btn-success .btn-lg role="button"}
62 |
64 | :::
65 |
66 | :::
67 |
68 | ::: {.hero-image .hero-image-right}
69 | :::
70 |
71 | ::::
72 |
73 | Welcome to the NHS Python Community’s python version of the NHS-Rplotthedots package.
74 |
75 | The [NHSR Plot the Dots](https://github.com/nhs-r-community/NHSRplotthedots) package was built by the [NHS-R community](https://nhsrcommunity.com) to provide tools for drawing statistical process control (SPC) charts. The package supports the NHS England programme ['Making Data Count'](https://www.england.nhs.uk/publication/making-data-count/). The programme encourages boards, managers, and analyst teams to present data in ways that show change over time, and drive better understanding of indicators than 'RAG' (red, amber, green) rated reports often present.
76 |
77 | ::: {.callout-note}
78 | ## Note
79 | Please be aware that this package is in the early stages of development, and features may change.
80 | :::
81 |
82 | ## Statistical Process Control
83 |
84 | Statistical process control (SPC) is an analytical technique that plots data over time. It helps us understand variation and in so doing guides us to take the most appropriate action.
85 |
86 | SPC is a good technique to use when implementing change as it enables you to understand whether changes you are making are resulting in improvement — a key component of the Model for Improvement widely used within the NHS.
87 |
88 | ::: {.panel-tabset}
89 | ## Example `nhspy-plotthedots` Chart
90 | ```{python}
91 | #| echo: false
92 | from nhspy_plotthedots import plotly_spc_chart
93 |
94 | # plot SPC chart
95 | plotly_spc_chart.plotly_spc_chart(spc, 'breaches', 'period', plot_title = 'Chelsea & Westminster Hospital NHS FT', x_lab = 'Month of attendance', y_lab = 'Number of 4-Hour Target Breaches')
96 | ```
97 | :::
98 |
99 | ## NHS-R slack
100 |
101 | If you want to learn more about this project, please join the discussion at the [NHS-R Community Slack group](https://nhsrcommunity.slack.com/) and the specific channel [#proj-nhsr-plot-the-dots](https://nhsrcommunity.slack.com/archives/CSVD4SYF3).
--------------------------------------------------------------------------------
/docs/_site/site_libs/quarto-html/anchor.min.js:
--------------------------------------------------------------------------------
1 | // @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat
2 | //
3 | // AnchorJS - v4.3.1 - 2021-04-17
4 | // https://www.bryanbraun.com/anchorjs/
5 | // Copyright (c) 2021 Bryan Braun; Licensed MIT
6 | //
7 | // @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat
8 | !function(A,e){"use strict";"function"==typeof define&&define.amd?define([],e):"object"==typeof module&&module.exports?module.exports=e():(A.AnchorJS=e(),A.anchors=new A.AnchorJS)}(this,function(){"use strict";return function(A){function d(A){A.icon=Object.prototype.hasOwnProperty.call(A,"icon")?A.icon:"",A.visible=Object.prototype.hasOwnProperty.call(A,"visible")?A.visible:"hover",A.placement=Object.prototype.hasOwnProperty.call(A,"placement")?A.placement:"right",A.ariaLabel=Object.prototype.hasOwnProperty.call(A,"ariaLabel")?A.ariaLabel:"Anchor",A.class=Object.prototype.hasOwnProperty.call(A,"class")?A.class:"",A.base=Object.prototype.hasOwnProperty.call(A,"base")?A.base:"",A.truncate=Object.prototype.hasOwnProperty.call(A,"truncate")?Math.floor(A.truncate):64,A.titleText=Object.prototype.hasOwnProperty.call(A,"titleText")?A.titleText:""}function w(A){var e;if("string"==typeof A||A instanceof String)e=[].slice.call(document.querySelectorAll(A));else{if(!(Array.isArray(A)||A instanceof NodeList))throw new TypeError("The selector provided to AnchorJS was invalid.");e=[].slice.call(A)}return e}this.options=A||{},this.elements=[],d(this.options),this.isTouchDevice=function(){return Boolean("ontouchstart"in window||window.TouchEvent||window.DocumentTouch&&document instanceof DocumentTouch)},this.add=function(A){var e,t,o,i,n,s,a,c,r,l,h,u,p=[];if(d(this.options),"touch"===(l=this.options.visible)&&(l=this.isTouchDevice()?"always":"hover"),0===(e=w(A=A||"h2, h3, h4, h5, h6")).length)return this;for(null===document.head.querySelector("style.anchorjs")&&((u=document.createElement("style")).className="anchorjs",u.appendChild(document.createTextNode("")),void 0===(A=document.head.querySelector('[rel="stylesheet"],style'))?document.head.appendChild(u):document.head.insertBefore(u,A),u.sheet.insertRule(".anchorjs-link{opacity:0;text-decoration:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}",u.sheet.cssRules.length),u.sheet.insertRule(":hover>.anchorjs-link,.anchorjs-link:focus{opacity:1}",u.sheet.cssRules.length),u.sheet.insertRule("[data-anchorjs-icon]::after{content:attr(data-anchorjs-icon)}",u.sheet.cssRules.length),u.sheet.insertRule('@font-face{font-family:anchorjs-icons;src:url(data:n/a;base64,AAEAAAALAIAAAwAwT1MvMg8yG2cAAAE4AAAAYGNtYXDp3gC3AAABpAAAAExnYXNwAAAAEAAAA9wAAAAIZ2x5ZlQCcfwAAAH4AAABCGhlYWQHFvHyAAAAvAAAADZoaGVhBnACFwAAAPQAAAAkaG10eASAADEAAAGYAAAADGxvY2EACACEAAAB8AAAAAhtYXhwAAYAVwAAARgAAAAgbmFtZQGOH9cAAAMAAAAAunBvc3QAAwAAAAADvAAAACAAAQAAAAEAAHzE2p9fDzz1AAkEAAAAAADRecUWAAAAANQA6R8AAAAAAoACwAAAAAgAAgAAAAAAAAABAAADwP/AAAACgAAA/9MCrQABAAAAAAAAAAAAAAAAAAAAAwABAAAAAwBVAAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAMCQAGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAg//0DwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAAIAAAACgAAxAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADAAAAAIAAgAAgAAACDpy//9//8AAAAg6cv//f///+EWNwADAAEAAAAAAAAAAAAAAAAACACEAAEAAAAAAAAAAAAAAAAxAAACAAQARAKAAsAAKwBUAAABIiYnJjQ3NzY2MzIWFxYUBwcGIicmNDc3NjQnJiYjIgYHBwYUFxYUBwYGIwciJicmNDc3NjIXFhQHBwYUFxYWMzI2Nzc2NCcmNDc2MhcWFAcHBgYjARQGDAUtLXoWOR8fORYtLTgKGwoKCjgaGg0gEhIgDXoaGgkJBQwHdR85Fi0tOAobCgoKOBoaDSASEiANehoaCQkKGwotLXoWOR8BMwUFLYEuehYXFxYugC44CQkKGwo4GkoaDQ0NDXoaShoKGwoFBe8XFi6ALjgJCQobCjgaShoNDQ0NehpKGgobCgoKLYEuehYXAAAADACWAAEAAAAAAAEACAAAAAEAAAAAAAIAAwAIAAEAAAAAAAMACAAAAAEAAAAAAAQACAAAAAEAAAAAAAUAAQALAAEAAAAAAAYACAAAAAMAAQQJAAEAEAAMAAMAAQQJAAIABgAcAAMAAQQJAAMAEAAMAAMAAQQJAAQAEAAMAAMAAQQJAAUAAgAiAAMAAQQJAAYAEAAMYW5jaG9yanM0MDBAAGEAbgBjAGgAbwByAGoAcwA0ADAAMABAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAP) format("truetype")}',u.sheet.cssRules.length)),u=document.querySelectorAll("[id]"),t=[].map.call(u,function(A){return A.id}),i=0;i\]./()*\\\n\t\b\v\u00A0]/g,"-").replace(/-{2,}/g,"-").substring(0,this.options.truncate).replace(/^-+|-+$/gm,"").toLowerCase()},this.hasAnchorJSLink=function(A){var e=A.firstChild&&-1<(" "+A.firstChild.className+" ").indexOf(" anchorjs-link "),A=A.lastChild&&-1<(" "+A.lastChild.className+" ").indexOf(" anchorjs-link ");return e||A||!1}}});
9 | // @license-end
--------------------------------------------------------------------------------
/tests/unittests/test_pandas_spc_x_calc.py:
--------------------------------------------------------------------------------
1 | # -------------------------------------------------------------------------
2 | # Copyright (c) 2023 NHS Python Community. All rights reserved.
3 | # Licensed under the MIT License. See license.txt in the project root for
4 | # license information.
5 | # -------------------------------------------------------------------------
6 |
7 | # FILE: test_pandas_spc_x_calc.py
8 |
9 | # DESCRIPTION: Tests on the pandas_scp_x_calc() function. Given a pandas DataFrame,
10 | # a string indicating the column name of the values to be analysed,
11 | # and an optional integer representing the number of values after which
12 | # the mean and other calculations should be fixed. It returns a pandas
13 | # DataFrame with the same values and the Statistic Process Control (SPC) values.
14 | # The SCP values const of:
15 | # - mean: The mean of the input values
16 | # - lpl: The lower process limit of the input values
17 | # - upl: The upper process limit of the input values
18 | # - outside_limits: A boolean list representing whether a value is
19 | # outside the process limits
20 | # - relative_to_mean: A list representing the relative value of an
21 | # element to mean
22 | # - close_to_limits: A boolean list representing whether a value is
23 | # close to a limit or not
24 | # - special_cause_flag: A boolean list representing whether a value
25 | # is a special cause or not 'outside_limits' representing whether a
26 | # value is outside the process limits
27 | #
28 | # CONTRIBUTORS: v.Morriss
29 | # CONTACT: -
30 | # CREATED: 9 Jul 2024
31 | # VERSION: 0.0.1
32 |
33 | # Imports
34 | # -------------------------------------------------------------------------
35 | # Python:
36 | import unittest
37 | import math
38 |
39 | # 3rd party:
40 | import pandas as pd
41 |
42 | # Local
43 | from nhspy_plotthedots.pandas_spc_calculations import pandas_spc_x_calc
44 |
45 | # Define tests
46 | # -------------------------------------------------------------------------
47 |
48 | class TestPandasSpcXCal(unittest.TestCase):
49 |
50 | def test_sample1(self):
51 | df = pd.DataFrame(data = {"column" : [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]})
52 | col = "column"
53 | n_points = 3
54 | expected = pd.DataFrame(data =
55 | {
56 | "column" : [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
57 | "mean" : [2.0] * 8,
58 | "lpl" : [-0.6600000000000001] * 8,
59 | "upl" : [4.66] * 8,
60 | "outside_limits" : [False] * 4 + [True] * 4,
61 | "relative_to_mean" : [-1.0, 0.0] + [1.0] * 6,
62 | "close_to_limits" : [False] * 3 + [True] + [False] * 4,
63 | "special_cause_flag" : [True] * 8,
64 | })
65 | self.assertTrue(pandas_spc_x_calc(df,col,n_points).equals(expected))
66 |
67 | def test_sample2(self):
68 | df = pd.DataFrame(data = {"column" : [16.0, 9.0, 4.0, 2.0, 1.0]})
69 | col = "column"
70 | n_points = 11
71 | expected = pd.DataFrame(data =
72 | {
73 | "column" : [16.0, 9.0, 4.0, 2.0, 1.0],
74 | "mean" : [6.4] * 5,
75 | "lpl" : [-3.575000000000001] * 5,
76 | "upl" : [16.375] * 5,
77 | "outside_limits" : [False] * 5,
78 | "relative_to_mean" : [1.0] * 2 + [-1.0] * 3,
79 | "close_to_limits" : [True] + [False] * 4,
80 | "special_cause_flag" : [False] * 5,
81 | })
82 | self.assertTrue(pandas_spc_x_calc(df,col,n_points).equals(expected))
83 |
84 | def test_sample3(self):
85 | df = pd.DataFrame(data = {"column" : [1,6,1,8,0,3]})
86 | col = "column"
87 | n_points = None
88 | expected = pd.DataFrame(data =
89 | {
90 | "column" : [1,6,1,8,0,3],
91 | "mean" : [3.1666666666666665] * 6,
92 | "lpl" : [-11.729333333333333] * 6,
93 | "upl" : [18.062666666666665] * 6,
94 | "outside_limits" : [False] * 6,
95 | "relative_to_mean" : [-1.0, 1.0, -1.0, 1.0, -1.0, -1.0],
96 | "close_to_limits" : [False] * 6,
97 | "special_cause_flag" : [False] * 6,
98 | })
99 | self.assertTrue(pandas_spc_x_calc(df,col,n_points).equals(expected))
100 |
101 | def test_nan(self):
102 | df = pd.DataFrame(data = {"column" : [math.nan]})
103 | col = "column"
104 | n_points = None
105 | expected = pd.DataFrame(data =
106 | {
107 | "column" : [math.nan],
108 | "mean" : [math.nan],
109 | "lpl" : [math.nan],
110 | "upl" : [math.nan],
111 | "outside_limits" : [False],
112 | "relative_to_mean" : [math.nan],
113 | "close_to_limits" : [False],
114 | "special_cause_flag" : [False],
115 | })
116 | self.assertTrue(pandas_spc_x_calc(df,col,n_points).equals(expected))
117 |
118 | def test_special_cause(self):
119 | pd.set_option('display.max_columns', None)
120 | df = pd.DataFrame(data = {"column" : [1,1,1,1,2]})
121 | col = "column"
122 | n_points = None
123 | expected = pd.DataFrame(data =
124 | {
125 | "column" : [1,1,1,1,2],
126 | "mean" : [1.2] * 5,
127 | "lpl" : [1.2] * 5,
128 | "upl" : [1.2] * 5,
129 | "outside_limits" : [True] * 5,
130 | "relative_to_mean" : [-1.0] * 4 + [1.0],
131 | "close_to_limits" : [False] * 5,
132 | "special_cause_flag" : [True] * 5,
133 | })
134 | self.assertTrue(pandas_spc_x_calc(df,col,n_points).equals(expected))
135 |
136 | if __name__ == '__main__':
137 | unittest.main()
--------------------------------------------------------------------------------
/nhspy_plotthedots/plotly_spc_chart.py:
--------------------------------------------------------------------------------
1 | # Python source
2 | # -------------------------------------------------------------------------
3 | # Copyright (c) 2023 NHS Python Community. All rights reserved.
4 | # Licensed under the MIT License. See license.txt in the project root for
5 | # license information.
6 | # -------------------------------------------------------------------------
7 |
8 | # FILE: plotly_spc_chart.py
9 |
10 | # DESCRIPTION: plotly_spc_chart() function.
11 |
12 | # CONTRIBUTORS: Craig R. Shenton
13 | # CONTACT: craig.shenton@nhs.net
14 | # CREATED: 22 Jan 2023
15 | # VERSION: 0.0.1
16 |
17 | # Imports
18 | # -------------------------------------------------------------------------
19 | # Python:
20 | from datetime import datetime
21 | from dateutil.relativedelta import relativedelta
22 |
23 | # 3rd party:
24 | import pandas as pd
25 | import plotly.graph_objects as go
26 |
27 | # Define get_status()
28 | # -------------------------------------------------------------------------
29 | def get_status(row: pd.Series) -> str:
30 | """
31 | Given a row of a dataframe, returns a string depending on the values
32 | of the 'outside_limits', 'close_to_limits' and 'relative_to_mean'
33 | columns of that row.
34 |
35 | :param row: A row of a dataframe
36 | :type row: pd.Series
37 | :return: A string indicating the status
38 | :rtype: str
39 | """
40 | if row['outside_limits']:
41 | return 'Outside Limit'
42 | elif row['close_to_limits']:
43 | return 'Close to limit'
44 | elif row['relative_to_mean'] > 0:
45 | return 'Above mean'
46 | elif row['relative_to_mean'] < 0:
47 | return 'Below mean'
48 | else:
49 | return ''
50 |
51 | # Define get_colour()
52 | # -------------------------------------------------------------------------
53 | def get_colour(row: pd.Series) -> str:
54 | """
55 | Given a row of a dataframe, returns a string depending on the values
56 | of the 'outside_limits', 'close_to_limits' columns of that row.
57 |
58 | :param row: A row of a dataframe
59 | :type row: pd.Series
60 | :return: A string indicating the colour
61 | :rtype: str
62 | """
63 | if row['outside_limits']:
64 | return 'red'
65 | elif row['close_to_limits']:
66 | return 'yellow'
67 | else:
68 | return 'rgb(22, 96, 167)'
69 |
70 | # Define plotly_spc_chart()
71 | # -------------------------------------------------------------------------
72 | def plotly_spc_chart(df: pd.DataFrame,
73 | values_col: str,
74 | date_col: str,
75 | plot_title: str,
76 | x_lab: str,
77 | y_lab: str) -> None:
78 | """
79 | This function creates a line chart using the specified dataframe, values
80 | column, date column, plot title, x-axis label, and y-axis label. The chart
81 | includes a scatter plot of the data points, a line plot of the mean, and
82 | shaded areas for the lower and upper control limits.
83 |
84 | Parameters:
85 | - df (pd.DataFrame): The dataframe to be plotted
86 | - values_col (str): The column name of the values to be plotted
87 | - date_col (str): The column name of the dates
88 | - plot_title (str): The title of the chart
89 | - x_lab (str): The label for the x-axis
90 | - y_lab (str): The label for the y-axis
91 |
92 | Returns:
93 | None
94 | """
95 | # Create a scatter plot of the data points
96 | scatter = go.Scatter(
97 | x=df[date_col],
98 | y=df[values_col],
99 | name = 'Performance',
100 | mode='lines+markers',
101 | marker=dict(
102 | color=df.apply(lambda row: get_colour(row), axis=1),
103 | size=10,
104 | symbol='circle'),
105 | line = dict(color = 'rgb(22, 96, 167)',
106 | width = 3, dash = 'solid'),
107 | text = df.apply(lambda row: get_status(row), axis=1),
108 | hovertemplate = '%{text}: %{y:.0f}',
109 | )
110 | # Create a line plot of the mean
111 | mean_line = go.Scatter(
112 | x=df[date_col],
113 | y=df['mean'],
114 | mode='lines',
115 | line = dict(color = 'rgba(174, 37, 115, 0.5)',
116 | width = 2,
117 | dash = 'dash'),
118 | name = "Mean",
119 | hovertemplate = 'mean: %{y:.0f}',
120 | )
121 | # Create a shaded area for the lower and upper control limits
122 | lpl_area = go.Scatter(
123 | x=df[date_col],
124 | y=df['lpl'],
125 | mode='lines',
126 | line=dict(
127 | color='rgba(174, 37, 115, 0.1)',
128 | width=0,
129 | ),
130 | name = "lpl",
131 | hovertemplate = 'lpl: %{y:.0f}',
132 | )
133 | upl_area = go.Scatter(
134 | x=df[date_col],
135 | y=df['upl'],
136 | mode='lines',
137 | line=dict(
138 | color='rgba(174, 37, 115, 0.1)',
139 | width=0,
140 | ),
141 | fill='tonexty',
142 | fillcolor='rgba(174, 37, 115, 0.1)',
143 | name = "upl",
144 | hovertemplate = 'upl: %{y:.0f}',
145 | )
146 | # Set options
147 | min_xaxis = min(df[date_col])
148 | max_xaxis = max(df[date_col])
149 | max_yaxis = max(df[values_col])
150 | remove = ['zoom2d','pan2d', 'select2d', 'lasso2d', 'zoomIn2d',
151 | 'zoomOut2d', 'autoScale2d', 'resetScale2d', 'zoom',
152 | 'pan', 'select', 'zoomIn', 'zoomOut', 'autoScale',
153 | 'resetScale', 'toggleSpikelines', 'hoverClosestCartesian',
154 | 'hoverCompareCartesian', 'toImage']
155 | # Set layout
156 | layout = go.Layout(title = plot_title,
157 | font = dict(size = 12),
158 | xaxis = dict(title = x_lab,
159 | # add more time to x-axis to show plot circles
160 | range = [min_xaxis - relativedelta(days=5),
161 | max_xaxis + relativedelta(days=5)]),
162 | yaxis = dict(title = y_lab,
163 | # fix y0 at 0 and add 10% to y1
164 | range = [0, max_yaxis + (max_yaxis * 0.1)]),
165 | showlegend = False,
166 | hovermode = "x unified")
167 | # Set configuration
168 | config = {'displaylogo': False,
169 | 'displayModeBar': True,
170 | 'modeBarButtonsToRemove': remove}
171 | # Create the figure and show()
172 | fig = go.Figure(data=[scatter, mean_line, lpl_area, upl_area], layout=layout)
173 | fig.update_layout(template='plotly_white')
174 | fig.show(config=config)
--------------------------------------------------------------------------------
/docs/_site/site_libs/quarto-nav/quarto-nav.js:
--------------------------------------------------------------------------------
1 | const headroomChanged = new CustomEvent("quarto-hrChanged", {
2 | detail: {},
3 | bubbles: true,
4 | cancelable: false,
5 | composed: false,
6 | });
7 |
8 | window.document.addEventListener("DOMContentLoaded", function () {
9 | let init = false;
10 |
11 | function throttle(func, wait) {
12 | var timeout;
13 | return function () {
14 | const context = this;
15 | const args = arguments;
16 | const later = function () {
17 | clearTimeout(timeout);
18 | timeout = null;
19 | func.apply(context, args);
20 | };
21 |
22 | if (!timeout) {
23 | timeout = setTimeout(later, wait);
24 | }
25 | };
26 | }
27 |
28 | function headerOffset() {
29 | // Set an offset if there is are fixed top navbar
30 | const headerEl = window.document.querySelector("header.fixed-top");
31 | if (headerEl) {
32 | return headerEl.clientHeight;
33 | } else {
34 | return 0;
35 | }
36 | }
37 |
38 | function footerOffset() {
39 | const footerEl = window.document.querySelector("footer.footer");
40 | if (footerEl) {
41 | return footerEl.clientHeight;
42 | } else {
43 | return 0;
44 | }
45 | }
46 |
47 | function updateDocumentOffsetWithoutAnimation() {
48 | updateDocumentOffset(false);
49 | }
50 |
51 | function updateDocumentOffset(animated) {
52 | // set body offset
53 | const topOffset = headerOffset();
54 | const bodyOffset = topOffset + footerOffset();
55 | const bodyEl = window.document.body;
56 | bodyEl.setAttribute("data-bs-offset", topOffset);
57 | bodyEl.style.paddingTop = topOffset + "px";
58 |
59 | // deal with sidebar offsets
60 | const sidebars = window.document.querySelectorAll(
61 | ".sidebar, .headroom-target"
62 | );
63 | sidebars.forEach((sidebar) => {
64 | if (!animated) {
65 | sidebar.classList.add("notransition");
66 | // Remove the no transition class after the animation has time to complete
67 | setTimeout(function () {
68 | sidebar.classList.remove("notransition");
69 | }, 201);
70 | }
71 |
72 | if (window.Headroom && sidebar.classList.contains("sidebar-unpinned")) {
73 | sidebar.style.top = "0";
74 | sidebar.style.maxHeight = "100vh";
75 | } else {
76 | sidebar.style.top = topOffset + "px";
77 | sidebar.style.maxHeight = "calc(100vh - " + topOffset + "px)";
78 | }
79 | });
80 |
81 | // allow space for footer
82 | const mainContainer = window.document.querySelector(".quarto-container");
83 | if (mainContainer) {
84 | mainContainer.style.minHeight = "calc(100vh - " + bodyOffset + "px)";
85 | }
86 |
87 | // link offset
88 | let linkStyle = window.document.querySelector("#quarto-target-style");
89 | if (!linkStyle) {
90 | linkStyle = window.document.createElement("style");
91 | linkStyle.setAttribute("id", "quarto-target-style");
92 | window.document.head.appendChild(linkStyle);
93 | }
94 | while (linkStyle.firstChild) {
95 | linkStyle.removeChild(linkStyle.firstChild);
96 | }
97 | if (topOffset > 0) {
98 | linkStyle.appendChild(
99 | window.document.createTextNode(`
100 | section:target::before {
101 | content: "";
102 | display: block;
103 | height: ${topOffset}px;
104 | margin: -${topOffset}px 0 0;
105 | }`)
106 | );
107 | }
108 | if (init) {
109 | window.dispatchEvent(headroomChanged);
110 | }
111 | init = true;
112 | }
113 |
114 | // initialize headroom
115 | var header = window.document.querySelector("#quarto-header");
116 | if (header && window.Headroom) {
117 | const headroom = new window.Headroom(header, {
118 | tolerance: 5,
119 | onPin: function () {
120 | const sidebars = window.document.querySelectorAll(
121 | ".sidebar, .headroom-target"
122 | );
123 | sidebars.forEach((sidebar) => {
124 | sidebar.classList.remove("sidebar-unpinned");
125 | });
126 | updateDocumentOffset();
127 | },
128 | onUnpin: function () {
129 | const sidebars = window.document.querySelectorAll(
130 | ".sidebar, .headroom-target"
131 | );
132 | sidebars.forEach((sidebar) => {
133 | sidebar.classList.add("sidebar-unpinned");
134 | });
135 | updateDocumentOffset();
136 | },
137 | });
138 | headroom.init();
139 |
140 | let frozen = false;
141 | window.quartoToggleHeadroom = function () {
142 | if (frozen) {
143 | headroom.unfreeze();
144 | frozen = false;
145 | } else {
146 | headroom.freeze();
147 | frozen = true;
148 | }
149 | };
150 | }
151 |
152 | // Observe size changed for the header
153 | const headerEl = window.document.querySelector("header.fixed-top");
154 | if (headerEl && window.ResizeObserver) {
155 | const observer = new window.ResizeObserver(
156 | updateDocumentOffsetWithoutAnimation
157 | );
158 | observer.observe(headerEl, {
159 | attributes: true,
160 | childList: true,
161 | characterData: true,
162 | });
163 | } else {
164 | window.addEventListener(
165 | "resize",
166 | throttle(updateDocumentOffsetWithoutAnimation, 50)
167 | );
168 | }
169 | setTimeout(updateDocumentOffsetWithoutAnimation, 250);
170 |
171 | // fixup index.html links if we aren't on the filesystem
172 | if (window.location.protocol !== "file:") {
173 | const links = window.document.querySelectorAll("a");
174 | for (let i = 0; i < links.length; i++) {
175 | links[i].href = links[i].href.replace(/\/index\.html/, "/");
176 | }
177 |
178 | // Fixup any sharing links that require urls
179 | // Append url to any sharing urls
180 | const sharingLinks = window.document.querySelectorAll(
181 | "a.sidebar-tools-main-item"
182 | );
183 | for (let i = 0; i < sharingLinks.length; i++) {
184 | const sharingLink = sharingLinks[i];
185 | const href = sharingLink.getAttribute("href");
186 | if (href) {
187 | sharingLink.setAttribute(
188 | "href",
189 | href.replace("|url|", window.location.href)
190 | );
191 | }
192 | }
193 |
194 | // Scroll the active navigation item into view, if necessary
195 | const navSidebar = window.document.querySelector("nav#quarto-sidebar");
196 | if (navSidebar) {
197 | // Find the active item
198 | const activeItem = navSidebar.querySelector("li.sidebar-item a.active");
199 | if (activeItem) {
200 | // Wait for the scroll height and height to resolve by observing size changes on the
201 | // nav element that is scrollable
202 | const resizeObserver = new ResizeObserver((_entries) => {
203 | // The bottom of the element
204 | const elBottom = activeItem.offsetTop;
205 | const viewBottom = navSidebar.scrollTop + navSidebar.clientHeight;
206 |
207 | // The element height and scroll height are the same, then we are still loading
208 | if (viewBottom !== navSidebar.scrollHeight) {
209 | // Determine if the item isn't visible and scroll to it
210 | if (elBottom >= viewBottom) {
211 | navSidebar.scrollTop = elBottom;
212 | }
213 |
214 | // stop observing now since we've completed the scroll
215 | resizeObserver.unobserve(navSidebar);
216 | }
217 | });
218 | resizeObserver.observe(navSidebar);
219 | }
220 | }
221 | }
222 | });
223 |
--------------------------------------------------------------------------------
/docs/_site/updates.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 | nhspy-plotthedots
9 | https://nhs-pycom.github.io/nhspy-plotthedots/updates.html
10 |
11 | Draw XmR charts in python for NHSE 'Making Data Count' programme.
12 | quarto-1.2.280
13 | Mon, 23 Jan 2023 00:00:00 GMT
14 |
15 | RAP Maturity Framework
16 | Craig R Shenton
17 | https://nhs-pycom.github.io/nhspy-plotthedots/posts/rap-maturity/index.html
18 |
23 |
The ‘Levels of RAP’ Maturity Framework
24 |
We are going to be developing nhspy-plotthedots in a Reproducible Analytical Pipeline (RAP) way by following the maturity framework developed by NHS Digital RAP community.
25 |
There are three levels to RAP:
26 |
27 |
Baseline - RAP fundamentals offering resilience against future change.
28 |
Silver - Implementing best practice by following good analytical and software engineering standards.
29 |
Gold - Analysis as a product to further elevate your analytical work and enhance its reusability to the public.
30 |
31 |
32 |
33 |
Baseline RAP - getting the fundamentals right
34 |
In order for a publication to be considered a reproducible analytical pipeline, it must at least meet all of the requirements of Baseline RAP:
35 |
36 |
Data produced by code in an open-source language (e.g., Python, R, SQL).
37 |
Code is version controlled (i.e., Git & GitHub).
38 |
Repository includes a README.md file (or equivalent) that clearly details steps a user must follow to reproduce the code.
39 |
Code has been peer reviewed (i.e., use PRs and code reviews)
40 |
Code is published in the open and linked to & from accompanying publication (if relevant).
41 |
42 |
43 |
44 |
Silver RAP - implementing best practice
45 |
Meeting all of the above requirements, plus:
46 |
47 |
Outputs are produced by code with minimal manual intervention.
48 |
Code is well-documented including user guidance, explanation of code structure & methodology and docstrings for functions.
49 |
Code is well-organised following standard directory format.
50 |
Reusable functions and/or classes are used where appropriate.
51 |
Code adheres to agreed coding standards (e.g., PEP8).
52 |
Pipeline includes a testing framework (unit tests, back tests).
53 |
Repository includes package dependency information.
54 |
Logs are automatically recorded by the pipeline to ensure outputs are as expected.
Repository automatically runs tests etc. via CI/CD or a different integration/deployment tool e.g. GitHub Actions.
64 |
Process runs based on event-based triggers (e.g., new data in database) or on a schedule.
65 |
Changes to the RAP are clearly signposted. E.g. a changelog in the package, releases etc. (See gov.uk info on Semantic Versioning)
66 |
67 |
68 |
69 |
70 |
71 | ]]>
72 | NHS
73 | RAP
74 | https://nhs-pycom.github.io/nhspy-plotthedots/posts/rap-maturity/index.html
75 | Mon, 23 Jan 2023 00:00:00 GMT
76 |
77 |
78 |
79 | nhspy-plotthedots alpha release
80 | Craig R Shenton
81 | https://nhs-pycom.github.io/nhspy-plotthedots/posts/welcome/index.html
82 | We are excited to announce the release of nhspy-plotthedots alpha, version 0.1.6. This new package is now available on test PyPI at https://test.pypi.org/project/nhspy-plotthedots-test/ for users to test and provide feedback.
87 |
In addition to the package release, the documentation site for nhspy-plotthedots is now live. This quarto site will provide detailed information on how to use the package, including installation instructions, usage examples, and API reference.
88 |
We look forward to hearing your feedback on nhspy-plotthedots and hope you find it as useful as we do.
89 |
90 |
91 |
92 |
93 |
94 |
95 | Note
96 |
97 |
98 |
99 |
This is an alpha release for testing, not for use in production.