Welcome to the Healthcare Services Analytics & Decision Science Atlas. This is a directory of open-source tools, packages, and projects for analytics and decision science in healthcare.
This site is in its very early stages - please check back as we continue to develop it.
In time, we hope this can develop into a tool to help showcase the amazing work happening across the analytics and data science communities in healthcare, promoting reuse and collaborative work on the tools we all need.
Sammi Rosser 💻 🖋 🎨 🤔 🚇 🚧 👀 |
Amy Heather 💻 🖋 📖 🎨 🚧 |
Zella King 🐛 |
padawan 🖋 |
ReyTan8 🖋 |
Contributions are very welcome!
Interested in discussing some aspect of the Atlas? Got an opinion on how it should be structured, the sort of thing it should showcase, how it's laid out, or the branding? For these higher-level discussions, head over to the discussions tab of our GitHub.
Spotted a bug? Got an idea for a potential improvement?
Please head over to our issues page and open a new issue - or see if you can contribute to any of the ones already open.
The Atlas has five kinds of entry: packages, projects and tools; books, training and communities (including collections of recordings); techniques; and graphs.
If you wish to submit a tool, package, project, book, training resource or community but are not confident with GitHub and Quarto (or you just have a simple submission to make), you can submit a GitHub issue via our template: https://github.com/hsma-programme/decision_intelligence_atlas/issues/new/choose
This will collect all the required information and use it to automatically generate a new folder and file in the correct format, raising it as a 'pull request' for inclusion in the atlas. A repository administrator will then review the auto-created file, make any tweaks and fixes required, and let you know when your contribution is live.
For techniques and graphs, you can suggest an entry with an issue, or write it yourself using a template (see below).
If you wish to have more control over your submission and are comfortable using Quarto, you may wish to take a fork of the repository, make your changes, and then submit a pull request. Your request will be reviewed and merged, with additions or tweaks possible.
There is a template for every kind of entry in the templates/ folder.
The templates README has full instructions. In short:
- Choose the template for your kind of entry from the table in the templates README.
- Copy the template folder into the matching section folder (e.g.
templates/techniques/→techniques/my_technique/), and rename it after your entry. Use only letters, numbers, hyphens (-) or underscores (_); no spaces or other special characters. - Fill in the YAML header of
index.qmd, using the comments for guidance. Choose categories and languages from the CSV files intemplates/, and setdateto today's date.- If no suitable category exists, add a new one in the YAML header and mention it in your pull request. An admin will review it and decide whether to add it to the list of categories.
- If your entry does not fit under the existing headers on the Packages, Projects & Tools page, please let us know in your pull request and an admin can look at adding a new section to this page. Please let us know if you have a section heading in mind.
- Write the page below the header, and add any images to the same folder, using relative links - for example:
- Good:
 - Bad:
 - Bad:

- Good:
- Run
python resources/scripts/check_yaml.pyto check the header, and preview the page (see below).
Books, courses, training resources and communities go in books_training/, and their categories decide which section of the Books, Training and Communities page they appear in. The books and training template explains the rules, which are also checked by check_yaml.py.
You are also welcome to suggest edits to an existing entry via a pull request. Each entry is a folder in packages_projects_tools/, books_training/, techniques/ or graph_catalogue/. Inside the folder, you'll find an index.qmd file - this is where you can make edits to the tags and details.
You can look at the URL to find out the filepath you need to look for.
e.g. The page for Vidigi has the URL https://atlas.hsma.co.uk/packages_projects_tools/vidigi/.
The file you need to edit is packages_projects_tools/vidigi/index.qmd
Additional images or other resources can be placed in the same folder and linked to, using relative links as above.
To preview your entry locally, run quarto preview to compile and display website.
The site is built with Quarto 1.7.33, so we recommend using that version, though most newer versions of Quarto should also work.
Tip
You do not need to render the site manually other than for the purpose of checking your page renders as expected - rerendering and deployment is handled automatically by GitHub Actions when your pull request is accepted.
If you wish to avoid having to clone the repository to your local machine and set up Quarto, you may like to try editing in GitHub codespaces. All GitHub accounts have a fairly generous free number of minutes available for codespace usage per month.
Take a fork as normal using the fork button on the repository.
Click on <> Code, then go to the Codespaces tab and select Create codespace on main.
It will take a few minutes, but you should then be presented with a web-based version of VSCode with the repository cloned and the appropriate version of Quarto pre-installed. You can then use the built-in GitHub features of the web-based VSCode to commit your changes to your fork and make a pull request when you are ready.
The Atlas lists recordings of conference talks, webinars, workshops and lectures from several YouTube playlists. Each set of recordings has its own Atlas entry with a searchable table of talks (for example, NHS-OA Community: Conference Recordings), and all of them are combined in the Recordings Finder.
Don't want to touch the code? Raise an issue with a link to the playlist or channel, and we'll add it for you.
resources/recordings/collections.ymllists every collection of recordings. Each collection belongs to one Atlas entry, and lists the YouTube playlists (or individual videos) it contains.resources/scripts/fetch_youtube_playlists.pyuses the YouTube Data API to fetch the title, description, date and thumbnail of every video, writing them to avideos.jsonfile in each collection's entry folder. It also records the date it fetched them asrecordings-checkedin a_metadata.ymlfile in the same folder (and inrecordings/when every collection is fetched), which the page shows above its table with{{< meta recordings-checked >}}.resources/scripts/build_youtube_talks.pyturns eachvideos.jsoninto atalks.ymlfile, with one row per talk, which is shown as a searchable table on the entry. It also writesrecordings/recordings.json, which the Recordings Finder reads.- The Update YouTube recordings GitHub Action runs both scripts for every collection every Monday, commits any changes and republishes the site, using the Atlas's API key stored as a repository secret. It can also be run by hand, e.g. straight after a conference.
For example, to add next year's conference recordings, add the playlist to the collection's playlists in resources/recordings/collections.yml:
nhs_oa_conferences:
folder: books_training/nhs_oa_conference_recordings
source: NHS-OA Community
playlists:
- label: RPySOC 2026
id: PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
type: Conference talk
# ...existing playlistsThe playlist ID is the part of the playlist's URL after list=. Playlists are listed on the entry and in the Recordings Finder in the order they appear in the file, so put the newest first.
Then fetch and build the recordings, preview the entry and the Recordings Finder, and check how recordings were split into talks.
-
Create the Atlas entry. Copy the recordings collection template into
books_training/, following the instructions for creating a new entry. The template already has the table of talks set up. Tag it withCourses and Training or Reference Materialsand either:Recorded Talks, for talks, webinars and conference recordings, so it appears in the 'Recorded webinars and conference talks' section of the Books, Training and Communities page, orCoursesandAsynchronous, for recorded training courses or workshops that can be followed at your own pace (e.g.books_training/mdsn_training), so it appears with the other courses.
Recordings can also be added to an existing entry instead, such as training videos for a tool - for example, the New Hospital Programme Demand Model entry in
packages_projects_tools/new_hospital_programmehas a table of its training recordings. If the entry's folder isn't inbooks_training/,packages_projects_tools/orrecordings/, also add it to thegit addline in.github/workflows/update-youtube-recordings.yml, or the weekly refreshes of its recordings won't be committed.The template already includes the table of talks. If you're adding recordings to an existing entry instead, add a listing to the entry's YAML header to show the table:
listing: - id: talk-recordings contents: talks.yml type: table fields: [title, year, duration, description] field-display-names: title: "Talk" year: "Year" duration: "Duration" description: "Details" sort: "date desc" filter-ui: [title, year, description] sort-ui: [title, year, duration] page-size: 25
and show it in the body of the entry:
## Search the talks *Last checked against YouTube on {{< meta recordings-checked >}} - the recordings are checked every week.* ::: {#talk-recordings} :::The "Last checked" line shows the date the recordings were last fetched from YouTube, taken from the
_metadata.ymlfile that the fetch script writes to the entry's folder.If the collection has more than one playlist, you can add
eventtofieldsto show each playlist's label (e.g. the conference or round), and usesort: falseto keep the order fromcollections.yml. See the existing recording entries, such asbooks_training/nhs_oa_conference_recordings/index.qmd, for examples. -
Add the collection to
resources/recordings/collections.yml:my_new_collection: folder: books_training/my_new_collection source: Name of the community or organisation playlists: - label: Webinars id: PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx type: Webinar
-
Add the entry to the Sources list at the bottom of
recordings/index.qmd. -
Fetch and build the recordings, then preview the new entry and the Recordings Finder. Check how recordings were split into talks, and check for duplicates of talks already in the Atlas.
If you don't have a YouTube API key, you can still build: the build script will create an empty table for the new collection. Its videos can then be fetched by running the GitHub Action on your branch, or will be fetched by the weekly run once your pull request is merged.
Each collection in collections.yml has:
| Option | Required? | Description |
|---|---|---|
folder |
Yes | The folder of the collection's Atlas entry. |
source |
Yes | Who published the recordings, shown in the Recordings Finder's Source column and filter. Collections from the same community should use the same source. |
playlists |
Yes | The playlists in the collection (see below). |
earlier_source |
No | A before date and a source, giving the source of videos published before that date, e.g. for a community that has been renamed or merged. For example, the NHS-OA collections show videos published before 2026 as from the NHS-R Community. |
split_talks |
No | Set to false to keep every video in the collection as one row, rather than splitting recordings into talks (see below). Used for the HSMA lecture recordings, where timestamps mark sections of a lecture rather than separate talks. |
keep_whole_videos |
No | A list of video IDs to keep as one row, in collections where other recordings should still be split, e.g. a workshop whose timestamps mark its sections. |
exclude_playlists |
No | Leave out any videos that are also in these playlists, e.g. to avoid listing the same talk in two collections. |
exclude_videos |
No | Leave out these videos, e.g. promotional videos, untitled recordings or short welcome and closing speeches. Takes effect when the talk tables are next built, without fetching again. |
Each playlist has the following options. Most are only needed to tidy up playlists whose titles or descriptions are messy, so start with label, id and type, then check the built table and add others if needed.
What to fetch
| Option | Required? | Description |
|---|---|---|
id |
Yes, unless using videos |
The YouTube playlist ID, or a list of IDs to combine several playlists under one label (e.g. separate playlists for each day or room of a conference). To include all of a channel's uploads, use its uploads playlist ID: its channel ID with UC at the start replaced by UU. |
videos |
No | A list of video IDs to include in place of a playlist, e.g. to pick a few videos out of a larger playlist. |
How the videos are labelled
| Option | Required? | Description |
|---|---|---|
label |
Yes | The playlist's name, shown in the Event column when a collection has more than one event. Must be unique within the collection. If it contains a year (e.g. RPySOC 2025), that year is used for all of its videos on the collection's entry; otherwise each video's upload date is used. See Years and dates. |
source |
No | The source shown for the playlist's videos in the Recordings Finder, in place of the collection's (and its earlier_source), e.g. for a playlist from a different community. |
event |
No | The event shown for the playlist's videos, in place of one taken from the label, e.g. so that extra videos added with videos are shown as part of the same series. |
events_by_title |
No | For playlists covering several courses or series: a list of pattern (a regular expression) and event. Videos whose titles match a pattern get that event (e.g. the course name) instead of one taken from the label, shown in the Event column. |
type |
Yes | The kind of recording (e.g. Conference talk, Webinar, Workshop, Lecture), shown in the Recordings Finder's Type column and filter. Reuse an existing type where one fits. |
types_by_title |
No | For playlists mixing different kinds of recording: a list of pattern (a regular expression) and type. Videos whose titles match a pattern get that type instead. |
no_year |
No | Set to true to leave the year blank on the collection's entry, e.g. where videos were uploaded long after the event. This doesn't affect the Recordings Finder, which always shows the YouTube publish date - see Years and dates. |
Tidying titles and descriptions
| Option | Required? | Description |
|---|---|---|
remove_from_title |
No | A regular expression removed from each video's title, e.g. '^INSIGHT 2020:\s*' to remove a prefix repeated in every title. Use single quotes in the YAML for patterns containing backslashes. |
remove_from_description |
No | A list of regular expressions. Paragraphs of each video's description that match any of them are removed, e.g. an introduction to the event series repeated in every description. |
title_from_description |
No | Set to true to use the first line of each video's description as its title, for playlists whose video titles are cluttered or cut short (e.g. HACA2025 - Day 1 - Main Stage) but whose descriptions start with the talk title. |
Leaving videos out
| Option | Required? | Description |
|---|---|---|
exclude_by_title |
No | A list of regular expressions. Videos whose titles match any of them are left out, e.g. regular admin sessions that will keep being added to the playlist. |
Splitting recordings into talks
| Option | Required? | Description |
|---|---|---|
talk_list |
No | Set to true if session recordings list their talks one per line without timestamps (see below). |
Notes on the options that take regular expressions:
- All patterns ignore case.
- For
types_by_titleandevents_by_title, the first rule whose pattern matches is used, so put more specific patterns first. exclude_by_title,types_by_titleandevents_by_titlematch the video's original title on YouTube, beforeremove_from_titleortitle_from_descriptionchange it.- In YAML, put patterns containing backslashes in single quotes (e.g.
'^INSIGHT 2020:\s*'), as double quotes treat backslashes as escape characters.
Some recordings cover a whole session or day with several talks. Where a recording's description lists the talks, build_youtube_talks.py splits it into one row per talk:
- Timestamps, such as
34:23 Speaker - Talk title,1:02:03 Talk titleor1. (0:30) Talk title, become separate rows whose links start the video at that talk. - Programme times, such as
09:30 Speaker - Talk title, are times of day rather than positions in the video, so their rows link to the start of the recording. - Plain lists of talks, one per line with no times, are split into rows for playlists with
talk_list: true, linking to the start of the recording.
Items that aren't talks, such as breaks, lunch and Q&A, are left out when splitting, as are welcomes, thank-yous and opening or closing remarks shorter than 5 minutes. Longer ones, and ones whose length isn't known, are kept.
Recordings that don't list their talks are kept as one row, as are all recordings in collections with split_talks: false and any videos listed in keep_whole_videos.
Timestamps don't always mean separate talks. Many lectures, workshops and training sessions have timestamps that mark their sections (e.g. 55:23 Using Projects, 1:18:00 Introduction to ggplot2) as chapters for viewers. These get split into rows too, which fills the tables with fragments that aren't useful on their own. So after building, check which recordings were split:
python -c "import json, collections, re; c = collections.Counter((r['collection'], re.sub(r'&t=\d+s', '', r['url'])) for r in json.load(open('recordings/recordings.json', encoding='utf-8'))); [print(n, col, url) for (col, url), n in c.most_common() if n > 1]"
This lists each recording split into more than one row, with the number of rows. For each one, ask whether the rows are separate talks, usually by different speakers, that someone might want to find on their own:
- Separate talks, such as conference sessions, lightning talks or a branch meeting with several presentations: leave them split.
- Sections of one talk, such as a lecture or workshop: keep the recording as one row. Add its video ID to the collection's
keep_whole_videos, or, if the collection is mostly lectures or workshops (like the HSMA lecture recordings), setsplit_talks: falseon the collection. - A short clip that isn't a talk, such as a welcome, a closing speech or an introduction to the day, uploaded as its own video: add its video ID to
exclude_videos. Don't useexclude_by_titlefor a row split out of a longer recording - it matches the title of the whole recording, so it would leave out every talk in it.
If a recording of several talks isn't split, check its description on YouTube. The best fix is usually to add timestamps to the description there, which also gives viewers chapters to jump between.
The weekly refresh splits new videos in existing playlists in the same way, so it's worth running this check every so often, as new recordings are added.
The collection entries and the Recordings Finder date recordings differently:
- Collection entries show a year. It comes from the playlist's
labelif it contains one (e.g.RPySOC 2025), or otherwise from the year each video was uploaded. It's left blank for playlists withno_year: true. - The Recordings Finder shows the date each recording was published on YouTube, and its slider filters on that date. This is the only date available for every recording, so
labelyears andno_yeardon't affect it.
Most recordings are published within a few weeks of the event, so the two usually agree, but watch out for:
- Recordings uploaded long after the event, which show their upload date in the Recordings Finder, even if their year is blank on their entry. For example, HSMA project showcases from earlier rounds show when they were uploaded, rather than when the round ran.
- Events late in the year whose recordings were published early the following year. For example, RPySOC 2025 talks show 2025 on their entry, but January 2026 in the Recordings Finder.
If a collection's dates could mislead, say so on its entry, e.g. "These recordings were uploaded in 2022, after HSMA 4 ran".
Each talk's duration (HH:MM:SS) comes from YouTube's length for the video. For talks split out of a longer recording by timestamps, it's the time from the talk's timestamp to the next one (including breaks) or to the end of the video, so the last talk in a recording may include closing remarks. Talks listed by programme times or with talk_list: true have no timestamps, so their duration is left blank, and the Recordings Finder only shows them when its duration slider covers its full range.
The same talk is often in more than one playlist, e.g. conference workshops that are also in a community's workshop playlist. Some duplicates are removed automatically:
- The same video in more than one playlist of a collection is only listed once, in the first of those playlists in
collections.yml. - The same talk in more than one collection is listed on each collection's entry, so each entry shows its whole collection, but only once in the Recordings Finder, under the first of those collections in
collections.yml. A talk counts as the same if it has the same link and title.
Others need to be left out by hand, with exclude_videos or exclude_playlists:
- The same talk uploaded more than once, as separate videos with different IDs.
- The same video with different titles in different collections, e.g. because only one of them uses
remove_from_titleortitle_from_description.
After building, you can check the Recordings Finder for links listed more than once with:
python -c "import json, collections; c = collections.Counter(r['url'] for r in json.load(open('recordings/recordings.json', encoding='utf-8'))); print([u for u, n in c.items() if n > 1])"
Links listed more than once aren't always a problem: talks split from a session recording with talk_list all link to the start of the recording. Check whether the titles match before leaving anything out.
Fetching the videos' details from YouTube needs a YouTube Data API key. There are two ways to do it.
The Update YouTube recordings GitHub Action uses the Atlas's own API key, which is stored securely in the repository as the YOUTUBE_API_KEY secret. It commits to main using a GitHub App (stored as the RECORDINGS_APP_ID variable and RECORDINGS_APP_PRIVATE_KEY secret) that is allowed to bypass the rule that changes to main go through a pull request. As well as running automatically every Monday, anyone with write access to the repository can run it by hand:
- Go to the repository's Actions tab and choose Update YouTube recordings from the list of workflows.
- Click Run workflow, choose the branch to run it on, and click the green Run workflow button.
The Action fetches every collection, builds the tables of talks, and commits any changes to the branch it was run on.
- Run on
main(e.g. straight after a conference, to pick up new videos in an existing playlist), it then rebuilds and republishes the whole site, even if no recordings changed. - Run on another branch (e.g. the branch for a pull request adding a new collection), it commits the fetched recordings to that branch without publishing anything, so they can be reviewed before merging. It doesn't commit the "last checked" dates of existing collections, which change on every run, so that the branch doesn't conflict with the weekly runs on
main.
If you've contributed from a fork and don't have write access, you don't need to do anything - a maintainer will run the Action, or the weekly run will pick up your collection once your pull request is merged.
The GitHub App belongs to the organisation, not the repository, so after a transfer the Action is likely to fail. Either the Get token that can push to main step fails because the app isn't installed on the repository any more, or the push is rejected with GH013: Repository rule violations because the app isn't on the new ruleset's bypass list. To fix it, an owner of the organisations involved should:
- Move or recreate the app. In the old organisation, go to Settings > Developer settings > GitHub Apps, open the app, and use Transfer ownership under Advanced to move it to the new organisation. This keeps its App ID and private key, so the repository variable and secret stay valid. If that isn't possible, create a new app in the new organisation with Repository permissions > Contents: Read and write and nothing else, then generate a private key for it.
- Install the app on the new organisation, choosing Only select repositories and picking this repository.
- Check the Action's settings under the repository's Settings > Secrets and variables > Actions. There should be secrets
YOUTUBE_API_KEYandRECORDINGS_APP_PRIVATE_KEY(the full contents of the app's.pemprivate key file), and a variableRECORDINGS_APP_ID(the App ID, shown on the app's settings page). Re-add any that are missing, and update both app values if you created a new app. - Check the rules on
mainunder Settings > Rules > Rulesets. Rulesets set at the organisation level stay with the old organisation, and the new one may have its own. Whichever ruleset requires pull requests formain, add the app to its Bypass list as Always allow. - Check Actions are allowed under Settings > Actions > General. If the new organisation limits which actions can run, it needs to allow
actions/create-github-app-token.
Then run the Action by hand on main to check it works.
-
Get an API key. Keys are free. Create a project in the Google Cloud Console, enable the YouTube Data API v3, and create an API key under APIs & Services > Credentials. Under the key's API restrictions, restrict it to the YouTube Data API v3 only. Fetching every collection uses well under 1% of the free daily quota.
-
Make the key available to the scripts, as an environment variable called
YOUTUBE_API_KEY. The simplest way is a.envfile in the root of the project containing:YOUTUBE_API_KEY=your-key-hereThe scripts read this file automatically. Alternatively, set the environment variable in your terminal before running the scripts. This only lasts until you close the terminal:
- Windows (PowerShell):
$env:YOUTUBE_API_KEY = "your-key-here" - Windows (Command Prompt):
set YOUTUBE_API_KEY=your-key-here - macOS or Linux:
export YOUTUBE_API_KEY=your-key-here
or set it permanently for your user account, e.g. on Windows by running
[Environment]::SetEnvironmentVariable("YOUTUBE_API_KEY", "your-key-here", "User")in PowerShell and then restarting your terminal and code editor. If you're working in GitHub Codespaces, add the key as a Codespaces secret calledYOUTUBE_API_KEYinstead. - Windows (PowerShell):
-
Fetch the video details:
python resources/scripts/fetch_youtube_playlists.py [collection ...]Give the names of the collections to fetch (e.g.
nhs_oa_conferences), or leave them out to fetch all of them. This writes avideos.jsonfile to each collection's folder. -
Build the tables of talks. This doesn't need an API key:
python resources/scripts/build_youtube_talks.py
Both scripts need the pyyaml package, which is included in the project's environment.yaml. Commit the updated videos.json, talks.yml and _metadata.yml files, and recordings/recordings.json and recordings/_metadata.yml, along with your other changes.
Warning
Keep your API key secret. Anyone with your key can use up its quota, and could run up costs if billing is enabled on your Google Cloud project.
- Never commit your key. Don't put it in any of the Atlas's files except
.env, which is ignored by git. To double-check before committing, rungit check-ignore .env(which should print.env) and look throughgit statusandgit difffor your key. - Don't paste your key into issues, pull requests, discussions or chat tools - including AI assistants.
- Don't use
--bodyor similar to pass your key to command line tools, as it will be saved in your shell's history. - Restrict your key to the YouTube Data API v3 only, so it can't be used for anything else.
- If you think your key has been exposed, delete it in the Google Cloud Console straight away and create a new one. Deleting a key from the repository's history doesn't make it safe again, as it may already have been copied.
If you have any other suggestions about the website layout, content, or anything else, please raise an issue on the repository.
When your pull request is merged, you will be added to the contributors list by the pull request reviewer.
Reviewers: see guidance on allcontributors.org/docs/en/bot/usage
You may also be added for other reasons, like providing valuable input via issues or discussions.

