From 195cf911fe33e518c927f0472c9218e10dfd4c74 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Thu, 14 Aug 2025 19:09:37 +0100 Subject: [PATCH 01/11] initial commit for jupyter - creates notebook folder and import paths, dependencies to requirements with flag ['notebook'], creates _version.py for single source of version inherited by setup.py and notebook, update bump2version to update this. --- .bumpversion.cfg | 10 +- README.md | 97 ++++++++++- notebooks/Quickstart.ipynb | 253 +++++++++++++++++++++++++++ notebooks/ResearchTemplate.ipynb | 287 +++++++++++++++++++++++++++++++ rcpchgrowth/__init__.py | 3 +- rcpchgrowth/_version.py | 5 + setup.py | 20 ++- 7 files changed, 664 insertions(+), 11 deletions(-) create mode 100644 notebooks/Quickstart.ipynb create mode 100644 notebooks/ResearchTemplate.ipynb create mode 100644 rcpchgrowth/_version.py diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 91ede6a..d9e2a78 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,8 +1,10 @@ [bumpversion] current_version = 4.3.8 -tag = False commit = True +tag = True +tag_name = v{new_version} +message = chore: bump version {current_version} → {new_version} -[bumpversion:file:setup.py] -search = version="{current_version}" -replace = version="{new_version}" +[bumpversion:file:rcpchgrowth/_version.py] +search = __version__ = "{current_version}" +replace = __version__ = "{new_version}" diff --git a/README.md b/README.md index 9ed18f9..c283b87 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,100 @@ # RCPCH Digital Growth Python library -Please go to for all documentation +[![PyPI version](https://img.shields.io/pypi/v/rcpchgrowth.svg)](https://pypi.org/project/rcpchgrowth/) +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/rcpch/rcpchgrowth-python/live?labpath=notebooks%2FQuickstart.ipynb) +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/rcpch/rcpchgrowth-python?quickstart=1) + +Please go to for full documentation. Issues can be raised here + +--- + +## Installation + +Minimal (algorithm only): + +```bash +pip install rcpchgrowth +``` + +With notebook & plotting convenience dependencies: + +```bash +pip install "rcpchgrowth[notebook]" +``` + +The `notebook` extra currently pulls in: `pandas`, `matplotlib`, `jupyterlab`, `ipykernel`. + +To verify versions inside a Jupyter session: + +```python +import rcpchgrowth, pandas as pd, sys +print(rcpchgrowth.__version__, pd.__version__, sys.version) +``` + +## Example notebooks + +Example notebooks live in `notebooks/`: + +- `Quickstart.ipynb` – single measurement, small batch, simple plot. +- `ResearchTemplate.ipynb` – structured workflow for batch CSV processing (ages, SDS, centiles, quality flags, export). + +### Launch options + +- Binder badge above opens the Quickstart notebook (branch `live`). Binder builds from this repo's `requirements.txt`; to add notebook extras inside Binder run: + + ```bash + pip install "rcpchgrowth[notebook]" + ``` + +- Codespaces badge launches a ready cloud dev environment; open the notebooks folder afterwards. + +## Data handling / privacy + +Do NOT place identifiable patient data in a public fork or commit history. De‑identify and keep raw data outside version control. The research template includes guidance for exporting enriched results safely. + +## Basic usage (programmatic) + +```python +from datetime import date +from rcpchgrowth import Measurement + +sex = 'F' +dob = date(2022, 6, 15) +md = date(2024, 2, 1) +weight_kg = 12.3 + +measurement = Measurement(birth_date=dob, measurement_method='weight', observation_date=md, observation_value=weight_kg, reference='uk-who', gestation_weeks=40, gestation_days=0).measurement + +# Extracting the results from the measurement dictionary + +# Calculated ages +chronological_age_decimal_years = measurement['measurement_dates']["chronological_decimal_age"] +corrected_age_decimal_years = measurement['measurement_dates']["corrected_decimal_age"] +chronological_calendar_age = measurement['measurement_dates']["chronological_calendar_age"] # returns age as readable text in years, months, weeks and days +corrected_calendar_age = measurement['measurement_dates']["corrected_calendar_age"] # returns age as readable text in years, months, weeks and days +# This returns corrected gestational age in weeks if the baby was premature and is not yet term. +corrected_gestational_age = measurement['measurement_dates']["corrected_gestational_age"]["corrected_gestation_weeks"] +corrected_gestational_age = measurement['measurement_dates']["corrected_gestational_age"]["corrected_gestation_days"] + +# calculated SDS and centiles +corrected_weight_sds = measurement["measurement_calculated_values"]["corrected_sds"] +corrected_weight_centile = measurement["measurement_calculated_values"]["corrected_centile"] +chronological_weight_sds = measurement["measurement_calculated_values"]["chronological_sds"] +chronological_weight_centile = measurement["measurement_calculated_values"]["chronological_centile"] + +print(f"Age (decimal years): {chronological_age_decimal_years:.3f}") +print(f"Weight: {weight_kg} kg | SDS: {corrected_weight_sds:.2f} | Centile: {corrected_weight_centile:.1f}") +``` + +--- + +## Contributing + +See issues list and please open discussions before large changes. + +--- + +Copyright © Royal College of Paediatrics and Child Health diff --git a/notebooks/Quickstart.ipynb b/notebooks/Quickstart.ipynb new file mode 100644 index 0000000..c40c67b --- /dev/null +++ b/notebooks/Quickstart.ipynb @@ -0,0 +1,253 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "40a98feb", + "metadata": {}, + "source": [ + "# RCPCHgrowth Quickstart\n", + "\n", + "This notebook is for researchers wishing to use the RCPCHGrowth calculations without using the RCPCH digital growth charts API. This explainer will guide on how to:\n", + "\n", + "1. Inspect your environment & package versions.\n", + "2. Calculate a single z-score (SDS) & centile for a child's measurement.\n", + "3. Process a small batch of measurements from a pandas DataFrame.\n", + "4. Plot a simple growth trajectory against centile bands (weight example).\n", + "5. (Bonus) Export results.\n", + "\n", + "It also demonstrates good reproducibility practice: fixed versions, explicit unit notes, and data schema guidance.\n", + "\n", + "> IMPORTANT: Do not place real identifiable patient data into a public clone of this repository. Keep research data local & de‑identified.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "ae9a9912", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'python': '3.12.0', 'platform': 'macOS-15.6-arm64-arm-64bit', 'rcpchgrowth': 'unknown', 'rcpchgrowth_module_path': '/Users/eatyourpeas/Development/RCPCH repositories/growth charts/rcpchgrowth-python/rcpchgrowth/__init__.py', 'pandas': '2.3.1'}\n" + ] + } + ], + "source": [ + "# Environment & versions (works whether installed via pip or run from cloned repo)\n", + "import sys, platform, pathlib\n", + "import pandas as pd\n", + "\n", + "try:\n", + " import rcpchgrowth # normal case: installed package\n", + "except ImportError:\n", + " # Fallback: we're likely in a cloned repo and haven't pip-installed yet.\n", + " repo_root = pathlib.Path().resolve().parent # notebooks/ -> repo root parent\n", + " if (repo_root / 'setup.py').exists():\n", + " sys.path.insert(0, str(repo_root))\n", + " import rcpchgrowth\n", + " else:\n", + " raise # re-raise if truly not available\n", + "\n", + "print({\n", + " 'python': sys.version.split()[0],\n", + " 'platform': platform.platform(),\n", + " 'rcpchgrowth': getattr(rcpchgrowth, '__version__', 'unknown'),\n", + " 'rcpchgrowth_module_path': getattr(rcpchgrowth, '__file__', 'n/a'),\n", + " 'pandas': pd.__version__,\n", + "})" + ] + }, + { + "cell_type": "markdown", + "id": "efa69f52", + "metadata": {}, + "source": [ + "## 1. Single measurement example\n", + "We will compute a weight SDS (z-score) and centile for a fictitious child.\n", + "\n", + "Assumptions / Inputs (mandatory):\n", + "- `sex`: 'male' or 'female'\n", + "- `birth_date` - supplied as a python Date\n", + "- `observation_date` - the date the child was measured: supplied as a python Date\n", + "- `observation_value` - the measurement: note the units are standard - only cm or kg or kg/m2 are accepted.\n", + "- `measurement_method` - the type of measurement performed. Options include: `['height', 'weight' , 'bmi', 'ofc']`. (ofc = 'occipto-frontal circumference', bmi='body mass index'). Note this must be lower case.\n", + "\n", + "Optional Inputs\n", + "- `gestation_weeks`: integer. Defaults to 40 if not supplied\n", + "- `gestation_days`: integer. Defaults to 0 if not supplied\n", + "- `reference`: this defaults to UK-WHO. Other options include: `['trisomy-21', 'trisomy-21-aap', 'turner-syndrome', 'cdc', 'who']`. For more information on the dataset please see our (documentation)['https://growth.rcpch.ac.uk']\n", + "\n", + "The Measurement class performs all the calculations and returns the results as a python dictionary. It accepts the parameters above and returns the class object. The results are accessed through the `measurement` class attribute.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "7acb21cb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Age (decimal years): 1.632\n", + "Weight: 12.3 kg | SDS: 1.21 | Centile: 88.7\n" + ] + } + ], + "source": [ + "from datetime import date\n", + "from rcpchgrowth import Measurement\n", + "\n", + "sex = 'female'\n", + "dob = date(2022, 6, 15)\n", + "md = date(2024, 2, 1)\n", + "weight_kg = 12.3\n", + "\n", + "measurement = Measurement(sex=sex, birth_date=dob, measurement_method='weight', observation_date=md, observation_value=weight_kg, reference='uk-who', gestation_weeks=40, gestation_days=0).measurement\n", + "\n", + "# Extracting the results from the measurement dictionary\n", + "\n", + "# Calculated ages\n", + "chronological_age_decimal_years = measurement['measurement_dates'][\"chronological_decimal_age\"]\n", + "corrected_age_decimal_years = measurement['measurement_dates'][\"corrected_decimal_age\"]\n", + "chronological_calendar_age = measurement['measurement_dates'][\"chronological_calendar_age\"] # returns age as readable text in years, months, weeks and days\n", + "corrected_calendar_age = measurement['measurement_dates'][\"corrected_calendar_age\"] # returns age as readable text in years, months, weeks and days\n", + "# This returns corrected gestational age in weeks if the baby was premature and is not yet term.\n", + "corrected_gestational_age = measurement['measurement_dates'][\"corrected_gestational_age\"][\"corrected_gestation_weeks\"]\n", + "corrected_gestational_age = measurement['measurement_dates'][\"corrected_gestational_age\"][\"corrected_gestation_days\"]\n", + "\n", + "# calculated SDS and centiles\n", + "corrected_weight_sds = measurement[\"measurement_calculated_values\"][\"corrected_sds\"]\n", + "corrected_weight_centile = measurement[\"measurement_calculated_values\"][\"corrected_centile\"]\n", + "chronological_weight_sds = measurement[\"measurement_calculated_values\"][\"chronological_sds\"]\n", + "chronological_weight_centile = measurement[\"measurement_calculated_values\"][\"chronological_centile\"]\n", + "\n", + "print(f\"Age (decimal years): {chronological_age_decimal_years:.3f}\")\n", + "print(f\"Weight: {weight_kg} kg | SDS: {corrected_weight_sds:.2f} | Centile: {corrected_weight_centile:.1f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "02c8e972", + "metadata": {}, + "source": [ + "## 2. Batch processing with a DataFrame\n", + "For research datasets you typically have many rows. In this example we have created a miniature DataFrame to show vectorised processing.\n", + "\n", + "Expected columns (example):\n", + "sex = 'female'\n", + "dob = date(2022, 6, 15)\n", + "md = date(2024, 2, 1)\n", + "weight_kg = 12.3\n", + "\n", + "We'll compute age, retrieve growth data per row, and create SDS & centile columns.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c340aac4", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from datetime import date\n", + "from rcpchgrowth import Measurement\n", + "\n", + "rows = [\n", + " {'id': 'A', 'sex': 'F', 'dob': date(2022, 6, 15), 'measurement_date': date(2024, 2, 1), 'weight_kg': 12.3},\n", + " {'id': 'B', 'sex': 'M', 'dob': date(2021,11,10), 'measurement_date': date(2024, 2, 1), 'weight_kg': 15.0},\n", + " {'id': 'C', 'sex': 'F', 'dob': date(2023, 4, 5), 'measurement_date': date(2024, 2, 1), 'weight_kg': 8.4},\n", + "]\n", + "\n", + "df = pd.DataFrame(rows)\n", + "\n", + "def compute_row(row):\n", + " measurement = Measurement(birth_date=row.dob, measurement_method='weight', observation_date=row.measurement_date, observation_value=row.weight_kg, reference='uk-who', gestation_weeks=40, gestation_days=0).measurement\n", + " age = measurement['measurement_dates'][\"chronological_decimal_age\"]\n", + " corrected_weight_sds = measurement[\"measurement_calculated_values\"][\"corrected_sds\"]\n", + " corrected_weight_centile = measurement[\"measurement_calculated_values\"][\"corrected_centile\"]\n", + " return pd.Series({'age_decimal_years': age, 'weight_sds': corrected_weight_sds, 'weight_centile': corrected_weight_centile})\n", + "\n", + "calc = df.apply(compute_row, axis=1)\n", + "df = pd.concat([df, calc], axis=1)\n", + "df" + ] + }, + { + "cell_type": "markdown", + "id": "db5c4ee5", + "metadata": {}, + "source": [ + "## 3. Simple trajectory plot\n", + "We'll plot weight SDS over age for the batch. For richer visualisations you might overlay centile bands using `create_chart` or export to a separate plotting library.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1badcf6", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "plt.figure(figsize=(5,3))\n", + "plt.axhline(0, color='lightgray', lw=1)\n", + "plt.scatter(df['age_decimal_years'], df['weight_sds'], c=['tab:blue','tab:orange','tab:green'])\n", + "for _, r in df.iterrows():\n", + " plt.text(r['age_decimal_years']+0.01, r['weight_sds'], r['id'])\n", + "plt.xlabel('Age (decimal years)')\n", + "plt.ylabel('Weight SDS')\n", + "plt.title('Weight SDS trajectory (example)')\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "daba370b", + "metadata": {}, + "source": [ + "## 4. Export results\n", + "You can now export the enriched DataFrame (with SDS & centiles) to CSV for downstream stats or modelling.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "785d6b36", + "metadata": {}, + "outputs": [], + "source": [ + "# Export (disabled by default) - uncomment to write\n", + "# df.to_csv('example_results.csv', index=False)\n", + "print('DataFrame ready; uncomment export line to save to CSV.')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "rcpchgrowth", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/ResearchTemplate.ipynb b/notebooks/ResearchTemplate.ipynb new file mode 100644 index 0000000..d50c10f --- /dev/null +++ b/notebooks/ResearchTemplate.ipynb @@ -0,0 +1,287 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "04f63e8a", + "metadata": {}, + "source": [ + "# Research Template: Batch Growth Calculations\n", + "\n", + "Use this template to process a CSV of growth measurements and output SDS, centiles, and quality flags. Copy (do not edit) this template when starting a new analysis to preserve provenance.\n", + "\n", + "Sections:\n", + "1. Configuration & Environment\n", + "2. Load Input Data\n", + "3. Data Validation & Cleaning\n", + "4. Derive Ages & (Optional) Correct for Prematurity\n", + "5. Compute SDS & Centiles\n", + "6. Add Quality / Plausibility Flags\n", + "7. Summaries & Visual Quality Checks\n", + "8. Export Augmented Dataset\n", + "\n", + "> ALWAYS de‑identify: no names, NHS numbers, addresses, exact birth dates if not required. Consider offsetting all dates by a fixed random number of days per subject if sharing.\n" + ] + }, + { + "cell_type": "markdown", + "id": "7c171d73", + "metadata": {}, + "source": [ + "## 1. Configuration & Environment\n", + "Adjust paths & parameters here.\n", + "- INPUT_CSV: path to your de‑identified dataset\n", + "- OUTPUT_CSV: destination for augmented results\n", + "- REFERENCE_SET: choose 'uk_who' (default) or see other reference selector functions\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64bbcd08", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import sys, platform\n", + "import pandas as pd\n", + "import pathlib\n", + "\n", + "# Attempt to import installed package; fallback to local repo path (parent of notebooks/)\n", + "try:\n", + " import rcpchgrowth\n", + "except ImportError:\n", + " repo_root = pathlib.Path().resolve().parent\n", + " if (repo_root / 'setup.py').exists():\n", + " sys.path.insert(0, str(repo_root))\n", + " import rcpchgrowth\n", + " else:\n", + " raise\n", + "\n", + "from datetime import date\n", + "\n", + "INPUT_CSV = Path('YOUR_INPUT_FILE.csv') # <-- change\n", + "OUTPUT_CSV = Path('augmented_results.csv')\n", + "REFERENCE_SET = 'uk_who' # placeholder for switching logic\n", + "\n", + "print({'python': sys.version.split()[0], 'platform': platform.platform(), 'rcpchgrowth': getattr(rcpchgrowth,'__version__','unknown'), 'module_path': getattr(rcpchgrowth,'__file__','n/a'), 'pandas': pd.__version__})\n", + "print('Input file exists?', INPUT_CSV.exists())" + ] + }, + { + "cell_type": "markdown", + "id": "9839041e", + "metadata": {}, + "source": [ + "## 2. Load Input Data\n", + "Expected columns (rename your fields if needed):\n", + "- id (string / pseudonym)\n", + "- sex (\"M\"/\"F\")\n", + "- dob (YYYY-MM-DD)\n", + "- measurement_date (YYYY-MM-DD)\n", + "- weight_kg (optional if computing weight SDS)\n", + "- height_cm (optional if computing height SDS)\n", + "\n", + "Add any BMI or head circumference columns similarly.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc7ffe32", + "metadata": {}, + "outputs": [], + "source": [ + "if INPUT_CSV.exists():\n", + " df = pd.read_csv(INPUT_CSV, parse_dates=['dob','measurement_date'])\n", + "else:\n", + " # Create demo frame (remove in production)\n", + " from datetime import date\n", + " df = pd.DataFrame([\n", + " {'id':'A','sex':'F','dob':'2022-06-15','measurement_date':'2024-02-01','weight_kg':12.3,'height_cm':87.2},\n", + " {'id':'B','sex':'M','dob':'2021-11-10','measurement_date':'2024-02-01','weight_kg':15.0,'height_cm':93.4},\n", + " ])\n", + " df['dob'] = pd.to_datetime(df['dob'])\n", + " df['measurement_date'] = pd.to_datetime(df['measurement_date'])\n", + "\n", + "print(df.head())\n", + "print(df.dtypes)" + ] + }, + { + "cell_type": "markdown", + "id": "b92ae3d8", + "metadata": {}, + "source": [ + "## 3. Data Validation & Cleaning\n", + "Lightweight checks: required columns, duplicate ids + dates, missing values.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb58f627", + "metadata": {}, + "outputs": [], + "source": [ + "required = {'id','sex','dob','measurement_date'}\n", + "missing_cols = required - set(df.columns)\n", + "if missing_cols:\n", + " raise ValueError(f'Missing required columns: {missing_cols}')\n", + "\n", + "# Basic duplicates check\n", + "dup_mask = df.duplicated(subset=['id','measurement_date'])\n", + "if dup_mask.any():\n", + " print('Warning: duplicate id+measurement_date rows found')\n", + " display(df[dup_mask])\n", + "\n", + "# Simple missing values report\n", + "na_counts = df.isna().sum()\n", + "print('Missing values per column:\\n', na_counts)" + ] + }, + { + "cell_type": "markdown", + "id": "d842f943", + "metadata": {}, + "source": [ + "## 4. Derive Ages\n", + "We'll compute chronological decimal age for each measurement. (Add corrected age logic if you have gestational age data.)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b99a7c3f", + "metadata": {}, + "outputs": [], + "source": [ + "from rcpchgrowth import chronological_decimal_age\n", + "\n", + "df['age_decimal_years'] = [chronological_decimal_age(d.date(), m.date()) for d,m in zip(df['dob'], df['measurement_date'])]\n", + "df[['id','age_decimal_years']].head()" + ] + }, + { + "cell_type": "markdown", + "id": "b72fccf1", + "metadata": {}, + "source": [ + "## 5. Compute SDS & Centiles\n", + "We'll calculate weight and height SDS / centiles where data is present.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "737493ac", + "metadata": {}, + "outputs": [], + "source": [ + "from rcpchgrowth import sds_for_measurement, centile, select_reference_data_for_uk_who_chart\n", + "\n", + "def compute_sds(row, measurement_name, value_col):\n", + " val = row.get(value_col)\n", + " if pd.isna(val):\n", + " return pd.Series({f'{measurement_name}_sds': pd.NA, f'{measurement_name}_centile': pd.NA})\n", + " ref = select_reference_data_for_uk_who_chart(measurement_name, row.sex, row.age_decimal_years)\n", + " sds_val = sds_for_measurement(measurement_name, val, ref)\n", + " return pd.Series({f'{measurement_name}_sds': sds_val, f'{measurement_name}_centile': centile(sds_val)})\n", + "\n", + "for m, col in [('weight','weight_kg'), ('height','height_cm')]:\n", + " res = df.apply(lambda r: compute_sds(r, m, col), axis=1)\n", + " df = pd.concat([df, res], axis=1)\n", + "\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "67cffb66", + "metadata": {}, + "source": [ + "## 6. Quality / Plausibility Flags\n", + "Simple biologically implausible value (BIV) flags using SDS thresholds (+/- 6 as placeholder; adjust to policy) and missingness.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be8527c9", + "metadata": {}, + "outputs": [], + "source": [ + "def flag_biv(sds):\n", + " try:\n", + " return abs(sds) > 6\n", + " except TypeError:\n", + " return pd.NA\n", + "\n", + "for m in ['weight','height']:\n", + " df[f'{m}_biv_flag'] = df[f'{m}_sds'].apply(flag_biv)\n", + "\n", + "# Missingness summary\n", + "metrics = ['weight_kg','height_cm']\n", + "missing_summary = {m: df[m].isna().mean() for m in metrics}\n", + "print('Missingness fraction:', missing_summary)\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "3a09ee88", + "metadata": {}, + "source": [ + "## 7. Summaries & Visual Quality Checks\n", + "Distributions & simple trends help spot outliers.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "984fd990", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, axes = plt.subplots(1,2, figsize=(8,3))\n", + "axes[0].hist(df['weight_sds'].dropna(), bins=10, color='skyblue')\n", + "axes[0].set_title('Weight SDS')\n", + "axes[1].hist(df['height_sds'].dropna(), bins=10, color='salmon')\n", + "axes[1].set_title('Height SDS')\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "df[['weight_sds','height_sds']].describe()" + ] + }, + { + "cell_type": "markdown", + "id": "2e548876", + "metadata": {}, + "source": [ + "## 8. Export Augmented Dataset\n", + "Write out enriched data. DO NOT commit sensitive outputs.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f4c9b421", + "metadata": {}, + "outputs": [], + "source": [ + "# Uncomment to export\n", + "# df.to_csv(OUTPUT_CSV, index=False)\n", + "print('Augmented dataset ready. Uncomment export line to save.')" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/rcpchgrowth/__init__.py b/rcpchgrowth/__init__.py index b4f3b61..b91cf37 100644 --- a/rcpchgrowth/__init__.py +++ b/rcpchgrowth/__init__.py @@ -3,7 +3,7 @@ from .cdc import select_reference_data_for_cdc_chart from .centile_bands import centile_band_for_centile from .chart_functions import create_chart -from .constants import * +from .constants import * # noqa: F401,F403 from .date_calculations import chronological_decimal_age, corrected_decimal_age, chronological_calendar_age, estimated_date_delivery, corrected_gestational_age from .dynamic_growth import create_thrive_line, return_correlation, create_thrive_lines from .global_functions import centile, sds_for_measurement, measurement_from_sds, percentage_median_bmi, measurement_for_z, cubic_interpolation, linear_interpolation @@ -14,3 +14,4 @@ from .trisomy_21_aap import select_reference_data_for_trisomy_21_aap from .turner import select_reference_data_for_turner from .uk_who import select_reference_data_for_uk_who_chart +from ._version import __version__ # single-source version diff --git a/rcpchgrowth/_version.py b/rcpchgrowth/_version.py new file mode 100644 index 0000000..3e5e8fb --- /dev/null +++ b/rcpchgrowth/_version.py @@ -0,0 +1,5 @@ +__all__ = ["__version__"] + +# Single source of truth for the package version. +# Updated by bump2version. +__version__ = "4.3.8" diff --git a/setup.py b/setup.py index 6ede8c1..3243f89 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,16 @@ from setuptools import setup, find_packages -from os import path +from pathlib import Path -here = path.abspath(path.dirname(__file__)) +here = Path(__file__).parent.resolve() +long_description = (here / "README.md").read_text(encoding="utf-8") -with open(path.join(here, "README.md"), encoding="utf-8") as f: - long_description = f.read() +# Single-source version import (no full package import to avoid side-effects) +version_ns = {} +exec((here / "rcpchgrowth" / "_version.py").read_text(encoding="utf-8"), version_ns) setup( name="rcpchgrowth", - version="4.3.8", + version=version_ns["__version__"], description="SDS and Centile calculations for UK Growth Data", long_description=long_description, url="https://github.com/rcpch/digital-growth-charts/blob/master/README.md", @@ -25,6 +27,14 @@ packages=find_packages(), python_requires=">3.8", install_requires=["python-dateutil", "scipy"], + extras_require={ + "notebook": [ + "pandas>=1.5", + "matplotlib>=3.7", + "jupyterlab", + "ipykernel", + ] + }, include_package_data=True, project_urls={ "Bug Reports": "https://github.com/rcpch/rcpchgrowth-python/issues", From c1797b01bcc890283812c9adf5d7f09b3e6a71b4 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Thu, 14 Aug 2025 19:14:09 +0100 Subject: [PATCH 02/11] functioning measurement calculation --- notebooks/Quickstart.ipynb | 120 ++++++++++++++++++++++++++++++++++--- 1 file changed, 112 insertions(+), 8 deletions(-) diff --git a/notebooks/Quickstart.ipynb b/notebooks/Quickstart.ipynb index c40c67b..d9b031a 100644 --- a/notebooks/Quickstart.ipynb +++ b/notebooks/Quickstart.ipynb @@ -148,25 +148,111 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "c340aac4", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idsexdobmeasurement_dateweight_kgage_decimal_yearsweight_sdsweight_centile
0Afemale2022-06-152024-02-0112.31.6317591.21283688.740380
1Bmale2021-11-102024-02-0115.02.2258731.44383892.560774
2Cfemale2023-04-052024-02-018.40.826831-0.05902447.646639
\n", + "
" + ], + "text/plain": [ + " id sex dob measurement_date weight_kg age_decimal_years \\\n", + "0 A female 2022-06-15 2024-02-01 12.3 1.631759 \n", + "1 B male 2021-11-10 2024-02-01 15.0 2.225873 \n", + "2 C female 2023-04-05 2024-02-01 8.4 0.826831 \n", + "\n", + " weight_sds weight_centile \n", + "0 1.212836 88.740380 \n", + "1 1.443838 92.560774 \n", + "2 -0.059024 47.646639 " + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "import pandas as pd\n", "from datetime import date\n", "from rcpchgrowth import Measurement\n", "\n", "rows = [\n", - " {'id': 'A', 'sex': 'F', 'dob': date(2022, 6, 15), 'measurement_date': date(2024, 2, 1), 'weight_kg': 12.3},\n", - " {'id': 'B', 'sex': 'M', 'dob': date(2021,11,10), 'measurement_date': date(2024, 2, 1), 'weight_kg': 15.0},\n", - " {'id': 'C', 'sex': 'F', 'dob': date(2023, 4, 5), 'measurement_date': date(2024, 2, 1), 'weight_kg': 8.4},\n", + " {'id': 'A', 'sex': 'female', 'dob': date(2022, 6, 15), 'measurement_date': date(2024, 2, 1), 'weight_kg': 12.3},\n", + " {'id': 'B', 'sex': 'male', 'dob': date(2021,11,10), 'measurement_date': date(2024, 2, 1), 'weight_kg': 15.0},\n", + " {'id': 'C', 'sex': 'female', 'dob': date(2023, 4, 5), 'measurement_date': date(2024, 2, 1), 'weight_kg': 8.4},\n", "]\n", "\n", "df = pd.DataFrame(rows)\n", "\n", "def compute_row(row):\n", - " measurement = Measurement(birth_date=row.dob, measurement_method='weight', observation_date=row.measurement_date, observation_value=row.weight_kg, reference='uk-who', gestation_weeks=40, gestation_days=0).measurement\n", + " measurement = Measurement(sex=row.sex, birth_date=row.dob, measurement_method='weight', observation_date=row.measurement_date, observation_value=row.weight_kg, reference='uk-who', gestation_weeks=40, gestation_days=0).measurement\n", " age = measurement['measurement_dates'][\"chronological_decimal_age\"]\n", " corrected_weight_sds = measurement[\"measurement_calculated_values\"][\"corrected_sds\"]\n", " corrected_weight_centile = measurement[\"measurement_calculated_values\"][\"corrected_centile\"]\n", @@ -188,10 +274,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "e1badcf6", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Matplotlib is building the font cache; this may take a moment.\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "import matplotlib.pyplot as plt\n", "\n", From d749a9ee79acfb721fe3fc7d72706506d1519098 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Thu, 14 Aug 2025 19:21:02 +0100 Subject: [PATCH 03/11] refactor research template --- notebooks/ResearchTemplate.ipynb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/notebooks/ResearchTemplate.ipynb b/notebooks/ResearchTemplate.ipynb index d50c10f..27dda80 100644 --- a/notebooks/ResearchTemplate.ipynb +++ b/notebooks/ResearchTemplate.ipynb @@ -75,7 +75,7 @@ "## 2. Load Input Data\n", "Expected columns (rename your fields if needed):\n", "- id (string / pseudonym)\n", - "- sex (\"M\"/\"F\")\n", + "- sex (\"male\"/\"female\")\n", "- dob (YYYY-MM-DD)\n", "- measurement_date (YYYY-MM-DD)\n", "- weight_kg (optional if computing weight SDS)\n", @@ -97,8 +97,8 @@ " # Create demo frame (remove in production)\n", " from datetime import date\n", " df = pd.DataFrame([\n", - " {'id':'A','sex':'F','dob':'2022-06-15','measurement_date':'2024-02-01','weight_kg':12.3,'height_cm':87.2},\n", - " {'id':'B','sex':'M','dob':'2021-11-10','measurement_date':'2024-02-01','weight_kg':15.0,'height_cm':93.4},\n", + " {'id':'A','sex':'female','dob':'2022-06-15','measurement_date':'2024-02-01','weight_kg':12.3,'height_cm':87.2},\n", + " {'id':'B','sex':'male','dob':'2021-11-10','measurement_date':'2024-02-01','weight_kg':15.0,'height_cm':93.4},\n", " ])\n", " df['dob'] = pd.to_datetime(df['dob'])\n", " df['measurement_date'] = pd.to_datetime(df['measurement_date'])\n", @@ -177,15 +177,15 @@ "metadata": {}, "outputs": [], "source": [ - "from rcpchgrowth import sds_for_measurement, centile, select_reference_data_for_uk_who_chart\n", + "from rcpchgrowth import Measurement\n", "\n", "def compute_sds(row, measurement_name, value_col):\n", " val = row.get(value_col)\n", " if pd.isna(val):\n", " return pd.Series({f'{measurement_name}_sds': pd.NA, f'{measurement_name}_centile': pd.NA})\n", - " ref = select_reference_data_for_uk_who_chart(measurement_name, row.sex, row.age_decimal_years)\n", - " sds_val = sds_for_measurement(measurement_name, val, ref)\n", - " return pd.Series({f'{measurement_name}_sds': sds_val, f'{measurement_name}_centile': centile(sds_val)})\n", + " measurement = Measurement(sex=row.sex, birth_date=row.dob, measurement_method=measurement_name, observation_date=row.measurement_date, observation_value=val, reference='uk-who', gestation_weeks=40, gestation_days=0).measurement\n", + " sds_val = measurement.measurement_calculated_values.corrected_sds\n", + " return pd.Series({f'{measurement_name}_sds': sds_val, f'{measurement_name}_centile': measurement.measurement_calculated_values.corrected_centile})\n", "\n", "for m, col in [('weight','weight_kg'), ('height','height_cm')]:\n", " res = df.apply(lambda r: compute_sds(r, m, col), axis=1)\n", From d17f97fa3aaa3e12a30efaad03ee5482bf1cfb57 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Thu, 14 Aug 2025 21:33:12 +0100 Subject: [PATCH 04/11] migration from setup.py to pyproject.toml. deprecates bump2version, updates dockerfile to have a python environment with rcpchgrowth installed for development, the other which launches jupyter in the browser --- .bumpversion.cfg | 4 +- .github/workflows/python-publish.yml | 31 +++++++--- .../run-pytest-on-push-and-all-prs.yml | 45 +++++++------- Dockerfile | 54 +++++++++++++---- docker-compose.yml | 23 ++++++-- notebooks/Quickstart.ipynb | 4 +- pyproject.toml | 59 +++++++++++++++++++ rcpchgrowth/_version.py | 12 +++- requirements.txt | 8 +-- s/dev | 18 ++++++ s/notebooks | 20 +++++++ setup.py | 44 -------------- 12 files changed, 219 insertions(+), 103 deletions(-) create mode 100644 pyproject.toml create mode 100755 s/dev create mode 100755 s/notebooks delete mode 100644 setup.py diff --git a/.bumpversion.cfg b/.bumpversion.cfg index d9e2a78..c19c1c5 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -5,6 +5,4 @@ tag = True tag_name = v{new_version} message = chore: bump version {current_version} → {new_version} -[bumpversion:file:rcpchgrowth/_version.py] -search = __version__ = "{current_version}" -replace = __version__ = "{new_version}" +[bumpversion:file:VERSION] diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 2068cf5..b36ffbb 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -12,26 +12,39 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - - name: Install dependencies + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml', 'requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install build & test dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine + pip install build twine + # Install test/dev dependencies (pytest etc.) pip install -r requirements.txt + # Install the project (will pull runtime deps from pyproject) + pip install . + - name: Run pytest - run: | - pytest + run: pytest -q + + - name: Build (sdist & wheel) + run: python -m build - - name: Build and publish + - name: Publish to PyPI + if: startsWith(github.ref, 'refs/tags/') env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* + run: twine upload dist/* diff --git a/.github/workflows/run-pytest-on-push-and-all-prs.yml b/.github/workflows/run-pytest-on-push-and-all-prs.yml index f452071..c60efdf 100644 --- a/.github/workflows/run-pytest-on-push-and-all-prs.yml +++ b/.github/workflows/run-pytest-on-push-and-all-prs.yml @@ -12,30 +12,33 @@ on: jobs: deploy: - runs-on: ubuntu-latest # Runs tests on multiple python versions across the range we support strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v4 # latest as at March 2024 - - - name: Set up Python version ${{ matrix.python-version }} - uses: actions/setup-python@v5 # latest as at March 2024 - with: - python-version: ${{ matrix.python-version }} - - - name: Display Python version - run: python -c "import sys; print(sys.version)" - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel - pip install -r requirements.txt - - - name: Run pytest - run: | - pytest + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml', 'requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install .[notebook] # ensure optional extras still work + pip install -r requirements.txt + + - name: Run pytest + run: pytest -q diff --git a/Dockerfile b/Dockerfile index ad820c5..590d6a0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,48 @@ -FROM python:3.12 +FROM python:3.12-slim + +# Environment hygiene +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + DEBIAN_FRONTEND=noninteractive -# Set the working directory in the container WORKDIR /app -# Copy the requirements file into the container -COPY requirements.txt . +# System deps (slim image minimal; add build-essential if future compiled deps needed) +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + build-essential \ + gfortran \ + libopenblas-dev \ + pkg-config \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Copy metadata first for build cache +COPY pyproject.toml README.md ./ +COPY requirements.txt ./ +COPY rcpchgrowth/_version.py rcpchgrowth/_version.py + +# Copy the full source (needed for editable install) +COPY rcpchgrowth rcpchgrowth + +# Upgrade pip/setuptools/wheel first +RUN pip install --upgrade pip setuptools wheel + +# Install runtime + notebook deps first (ensures wheels pulled before editable wiring) +RUN pip install python-dateutil scipy pandas matplotlib jupyterlab ipykernel + +# Install dev/test tools +RUN pip install -r requirements.txt -# Create a virtual environment -RUN python -m venv /app/venv +# Finally perform editable install (should be fast now; minimal build isolation work) +RUN pip install -e . || (echo '--- Editable install failed; running pip debug ---' && python -m pip debug && exit 1) -# Upgrade pip and install the dependencies in the virtual environment -RUN /app/venv/bin/pip install --upgrade pip -RUN /app/venv/bin/pip install -r requirements.txt +# Bring in notebooks only after package install so they are never considered for package discovery +COPY notebooks notebooks -# Copy the rest of the application code -COPY . . +# Expose Jupyter port +EXPOSE 8888 -# Set the entrypoint to use the virtual environment's Python interpreter -CMD ["python3"] \ No newline at end of file +# Helpful default: open bash; docker-compose can override with jupyter lab +CMD ["bash"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index c78e49a..27c7608 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,26 @@ version: "3.12" services: - rcpchgrowth-python: + rcpchgrowth-dev: build: . + container_name: rcpchgrowth-dev volumes: - .:/app working_dir: /app - tty: true # Allocate a pseudo-TTY - stdin_open: true # Keep stdin open - container_name: rcpchgrowth-python \ No newline at end of file + command: bash + tty: true + stdin_open: true + + rcpchgrowth-notebooks: + build: . + container_name: rcpchgrowth-notebooks + volumes: + - .:/app + working_dir: /app/notebooks + ports: + - "8888:8888" + command: bash -c "jupyter lab --ip=0.0.0.0 --no-browser --NotebookApp.token='' --NotebookApp.password='' --allow-root" + environment: + - PYTHONPATH=/app + depends_on: + - rcpchgrowth-dev diff --git a/notebooks/Quickstart.ipynb b/notebooks/Quickstart.ipynb index d9b031a..21aa2a4 100644 --- a/notebooks/Quickstart.ipynb +++ b/notebooks/Quickstart.ipynb @@ -335,7 +335,7 @@ ], "metadata": { "kernelspec": { - "display_name": "rcpchgrowth", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -349,7 +349,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.0" + "version": "3.12.11" } }, "nbformat": 4, diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5406c64 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,59 @@ +[build-system] +requires = ["setuptools>=69", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "rcpchgrowth" +version = "4.3.8" +description = "SDS and Centile calculations for UK Growth Data" +keywords = ["growth charts", "anthropometry", "SDS", "centile", "UK-WHO", "UK90", "Trisomy 21", "Turner", "CDC"] +license = "AGPL-3.0-or-later" +authors = [ + { name = "RCPCH Incubator", email = "incubator@rcpch.ac.uk" }, +] +maintainers = [ + { name = "RCPCH Incubator", email = "incubator@rcpch.ac.uk" }, +] +requires-python = ">=3.8" +dependencies = [ + "python-dateutil", + "scipy", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: Medical Science Apps.", +] + +[project.urls] +Homepage = "https://github.com/rcpch/rcpchgrowth-python" +"Bug Reports" = "https://github.com/rcpch/rcpchgrowth-python/issues" +"API management" = "https://dev.rcpch.ac.uk" +Documentation = "https://growth.rcpch.ac.uk/products/python-library/" + +[project.optional-dependencies] +notebook = [ + "pandas>=1.5", + "matplotlib>=3.7", + "jupyterlab", + "ipykernel", +] + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +include = ["rcpchgrowth"] +exclude = ["notebooks"] + +[project.readme] +file = "README.md" +content-type = "text/markdown" + +# Future: you can move package-data patterns here instead of MANIFEST.in if desired: +# [tool.setuptools.package-data] +# rcpchgrowth = ["data_tables/**/*.csv", "data_tables/**/*.json"] diff --git a/rcpchgrowth/_version.py b/rcpchgrowth/_version.py index 3e5e8fb..0e8b5cd 100644 --- a/rcpchgrowth/_version.py +++ b/rcpchgrowth/_version.py @@ -1,5 +1,11 @@ __all__ = ["__version__"] -# Single source of truth for the package version. -# Updated by bump2version. -__version__ = "4.3.8" +try: + from importlib.metadata import version as _meta_version +except ImportError: # Python <3.8 fallback not needed, but kept minimal + from importlib_metadata import version as _meta_version # type: ignore + +try: + __version__ = _meta_version("rcpchgrowth") +except Exception: + __version__ = "0.0.0+unknown" diff --git a/requirements.txt b/requirements.txt index b0c0c68..364566d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ bump2version pytest -python-dateutil -scipy -matplotlib -setuptools + +# Runtime dependencies (python-dateutil, scipy, matplotlib) are declared in pyproject.toml +# and installed automatically when installing the package. setuptools is provided +# via the build-system section, so it is not needed here. diff --git a/s/dev b/s/dev new file mode 100755 index 0000000..8c5fa38 --- /dev/null +++ b/s/dev @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e +set -o pipefail + +# Launch an interactive shell in the dev container (build if needed) +# Usage: s/dev + +# Ensure images are built +docker compose build rcpchgrowth-dev + +# Start (detached) if not already running +if ! docker compose ps --status=running | grep -q rcpchgrowth-dev; then + docker compose up -d rcpchgrowth-dev +fi + +# Exec into container +exec docker compose exec rcpchgrowth-dev bash diff --git a/s/notebooks b/s/notebooks new file mode 100755 index 0000000..dec0cad --- /dev/null +++ b/s/notebooks @@ -0,0 +1,20 @@ +#!/bin/bash + +set -e +set -o pipefail + +# Launch Jupyter Lab in the notebooks service (build/start as needed) +# Usage: s/notebooks + +# Build images (only if missing / outdated) +docker compose build rcpchgrowth-notebooks + +# Start notebooks service (and dependency) in background +if ! docker compose ps --status=running | grep -q rcpchgrowth-notebooks; then + docker compose up -d rcpchgrowth-notebooks +fi + +# Show the access URL +echo "Jupyter Lab should now be available at: http://localhost:8888" + +echo "To stop: docker compose stop rcpchgrowth-notebooks" diff --git a/setup.py b/setup.py deleted file mode 100644 index 3243f89..0000000 --- a/setup.py +++ /dev/null @@ -1,44 +0,0 @@ -from setuptools import setup, find_packages -from pathlib import Path - -here = Path(__file__).parent.resolve() -long_description = (here / "README.md").read_text(encoding="utf-8") - -# Single-source version import (no full package import to avoid side-effects) -version_ns = {} -exec((here / "rcpchgrowth" / "_version.py").read_text(encoding="utf-8"), version_ns) - -setup( - name="rcpchgrowth", - version=version_ns["__version__"], - description="SDS and Centile calculations for UK Growth Data", - long_description=long_description, - url="https://github.com/rcpch/digital-growth-charts/blob/master/README.md", - author="@eatyourpeas, @marcusbaw, @statist7, RCPCH Incubator", - author_email="incubator@rcpch.ac.uk", - classifiers=[ - "Development Status :: 5 - Production/Stable", - "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3 :: Only", - "Topic :: Scientific/Engineering :: Medical Science Apps.", - ], - keywords="growth charts, anthropometry, SDS, centile, UK-WHO, UK90, Trisomy 21 (UK), Trisomy 21 (AAP), Turner, CDC", - packages=find_packages(), - python_requires=">3.8", - install_requires=["python-dateutil", "scipy"], - extras_require={ - "notebook": [ - "pandas>=1.5", - "matplotlib>=3.7", - "jupyterlab", - "ipykernel", - ] - }, - include_package_data=True, - project_urls={ - "Bug Reports": "https://github.com/rcpch/rcpchgrowth-python/issues", - "API management": "https://dev.rcpch.ac.uk", - "Source": "https://github.com/rcpch/rcpchgrowth-python", - }, -) From 1825ff869f582a1af689ab6f330a9e851b2b9359 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Thu, 14 Aug 2025 21:56:47 +0100 Subject: [PATCH 05/11] readme updates --- README.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c283b87..e5f0a06 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,29 @@ Issues can be raised here ## Installation -Minimal (algorithm only): +### Docker + +If you want to avoid setting up docker environments, there are shortcut scripts the create a dockerized environment with RCPCHGrowth already installed. + +First ensure the folders have the correct permissions: + +```bash +chmod +x s/dev s/notebooks +``` + +To generate a container with the installed package for development + +```bash +s/dev +``` + +To generate a container that launches the jupyter notebooks to port 8888 + +```bash +s/notebooks +``` + +### Minimal installation (assuming you have a python virtual env setup) ```bash pip install rcpchgrowth @@ -61,7 +83,7 @@ Do NOT place identifiable patient data in a public fork or commit history. De‑ from datetime import date from rcpchgrowth import Measurement -sex = 'F' +sex = 'female' dob = date(2022, 6, 15) md = date(2024, 2, 1) weight_kg = 12.3 From 35d9c40cd8cf5458ffd8cf1d710f21ef39a79300 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Fri, 15 Aug 2025 19:02:54 +0100 Subject: [PATCH 06/11] notebook improvements --- notebooks/Quickstart.ipynb | 122 +++++++++++++++++++++++++++++-- notebooks/ResearchTemplate.ipynb | 38 ++++------ pyproject.toml | 2 +- 3 files changed, 131 insertions(+), 31 deletions(-) diff --git a/notebooks/Quickstart.ipynb b/notebooks/Quickstart.ipynb index 21aa2a4..690b397 100644 --- a/notebooks/Quickstart.ipynb +++ b/notebooks/Quickstart.ipynb @@ -5,7 +5,7 @@ "id": "40a98feb", "metadata": {}, "source": [ - "# RCPCHgrowth Quickstart\n", + "# RCPCHGrowth Quickstart\n", "\n", "This notebook is for researchers wishing to use the RCPCHGrowth calculations without using the RCPCH digital growth charts API. This explainer will guide on how to:\n", "\n", @@ -22,7 +22,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 14, "id": "ae9a9912", "metadata": {}, "outputs": [ @@ -40,15 +40,16 @@ "import pandas as pd\n", "\n", "try:\n", - " import rcpchgrowth # normal case: installed package\n", + " import rcpchgrowth\n", "except ImportError:\n", - " # Fallback: we're likely in a cloned repo and haven't pip-installed yet.\n", - " repo_root = pathlib.Path().resolve().parent # notebooks/ -> repo root parent\n", - " if (repo_root / 'setup.py').exists():\n", + " import sys, pathlib\n", + " repo_root = pathlib.Path(__file__).resolve().parents[1] # notebooks/ -> project root\n", + " # Heuristic: does the package directory exist inside this root?\n", + " if (repo_root / \"rcpchgrowth\" / \"__init__.py\").is_file() and (repo_root / \"pyproject.toml\").is_file():\n", " sys.path.insert(0, str(repo_root))\n", " import rcpchgrowth\n", " else:\n", - " raise # re-raise if truly not available\n", + " raise ImportError(\"RCPCHGrowth package not found\")\n", "\n", "print({\n", " 'python': sys.version.split()[0],\n", @@ -78,8 +79,113 @@ "- `gestation_weeks`: integer. Defaults to 40 if not supplied\n", "- `gestation_days`: integer. Defaults to 0 if not supplied\n", "- `reference`: this defaults to UK-WHO. Other options include: `['trisomy-21', 'trisomy-21-aap', 'turner-syndrome', 'cdc', 'who']`. For more information on the dataset please see our (documentation)['https://growth.rcpch.ac.uk']\n", + " \n", + "Optional Inputs (no impact of results) - these simply pass values into the Measurement class object. They have no functional use, but are helpful when used in conjunction with the charting component.\n", + "- `event_text`: list. this is a list of strings which are comments to tag a given measurement/plot with contextual information \n", + "- `bone_age`: an estimated skeletal age calculated from xrays reported in decimal years\n", + "- `bone_age_sds`: an SDS for the bone age, based on references\n", + "- `bone_age_centile`: a centile for the bone age, based on references\n", + "- `bone_age_reference`: enum ['greulich-pyle', 'tanner-whitehouse-ii', 'tanner-whitehouse-iii', 'fels', 'bonexpert']\n", "\n", - "The Measurement class performs all the calculations and returns the results as a python dictionary. It accepts the parameters above and returns the class object. The results are accessed through the `measurement` class attribute.\n" + "The Measurement class performs all the calculations and returns the results as a python dictionary. It accepts the parameters above and returns the class object. The results are accessed through the `measurement` class attribute.\n", + "\n", + "Each child measurement should create an instance of the Measurement class. To initialize the class you pass in the parameters above and then access the measurement attribute (see example).\n", + "\n", + "The resulting Measurement class object is made up of the following elements:\n", + "\n", + "```python\n", + "{\n", + " birth_data: {\n", + " ...\n", + " },\n", + " bone_age: {\n", + " ...\n", + " },\n", + " child_observation_value: {\n", + " ...\n", + " },\n", + " events_data: {\n", + " ...\n", + " },\n", + " measurement_calculated_values: {\n", + " ...\n", + " },\n", + " measurement_dates: {\n", + " ...\n", + " },\n", + " plottable_data: {\n", + " ...\n", + " }\n", + "}\n", + "```\n", + "\n", + "Of these, the import items to access are:\n", + "\n", + "### `birth_data`\n", + "\n", + "Comprises the following keys - some items are returning user inputs for sense checking, others are calculated values:\n", + "\n", + "```python\n", + "{\n", + " birth_date: date, # [python date],\n", + " estimated_date_delivery: date, # [python data],\n", + " estimated_date_delivery_string: str, # [string],\n", + " gestation_weeks: int, # [accepts 23-42]\n", + " gestation_days: int, # [accepts 0-7]\n", + " sex: str # ['male' | 'female']\n", + "}\n", + "```\n", + "\n", + "### `measurement_dates`\n", + "\n", + "These are date calculations based on the birth date, observation date and gestation if supplied.\n", + "\n", + "```python\n", + "measurement_dates: {\n", + " chronological_calendar_age: str,\n", + " chronological_decimal_age: float, \n", + " corrected_calendar_age: str,\n", + " corrected_decimal_age: float,\n", + " corrected_gestational_age: {\n", + " corrected_gestation_weeks: int\n", + " corrected_gestation_days: int\n", + " };\n", + " comments:{\n", + " clinician_corrected_decimal_age_comment: str,\n", + " lay_corrected_decimal_age_comment: str,\n", + " clinician_chronological_decimal_age_comment: str,\n", + " lay_chronological_decimal_age_comment: str,\n", + " }\n", + " observation_date: str,\n", + " corrected_decimal_age_error: str,\n", + " chronological_decimal_age_error: str,\n", + "}\n", + "```\n", + "\n", + "### `measurement_calculated_values\n", + "\n", + "These are the sds and centile calculations. Note that corrected and chronological results are always provided, even if the gestation is 40+0 (when they will be the same).\n", + "\n", + "```python\n", + "measurement_calculated_values: {\n", + " chronological_centile: float,\n", + " chronological_centile_band: str,\n", + " chronological_measurement_error: str,\n", + " chronological_sds: float,\n", + " corrected_centile: float,\n", + " corrected_centile_band: str,\n", + " corrected_measurement_error: str,\n", + " corrected_percentage_median_bmi: float\n", + " chronological_percentage_median_bmi: float\n", + " corrected_sds: float,\n", + "}\n", + "```\n", + "\n", + "To access the values, you have to walk across the object, either using dot notation:\n", + " | `measurement.measurement_calculated_values.chronological_centile`\n", + "or square bracket notation:\n", + "| `measurement[\"measurement_calculated_values\"][\"chronological_centile\"]`\n", + "\n" ] }, { diff --git a/notebooks/ResearchTemplate.ipynb b/notebooks/ResearchTemplate.ipynb index 27dda80..0cea1b2 100644 --- a/notebooks/ResearchTemplate.ipynb +++ b/notebooks/ResearchTemplate.ipynb @@ -34,6 +34,12 @@ "- REFERENCE_SET: choose 'uk_who' (default) or see other reference selector functions\n" ] }, + { + "cell_type": "markdown", + "id": "99c28dd4", + "metadata": {}, + "source": [] + }, { "cell_type": "code", "execution_count": null, @@ -44,26 +50,14 @@ "from pathlib import Path\n", "import sys, platform\n", "import pandas as pd\n", - "import pathlib\n", - "\n", - "# Attempt to import installed package; fallback to local repo path (parent of notebooks/)\n", - "try:\n", - " import rcpchgrowth\n", - "except ImportError:\n", - " repo_root = pathlib.Path().resolve().parent\n", - " if (repo_root / 'setup.py').exists():\n", - " sys.path.insert(0, str(repo_root))\n", - " import rcpchgrowth\n", - " else:\n", - " raise\n", - "\n", + "import rcpchgrowth\n", "from datetime import date\n", "\n", "INPUT_CSV = Path('YOUR_INPUT_FILE.csv') # <-- change\n", "OUTPUT_CSV = Path('augmented_results.csv')\n", - "REFERENCE_SET = 'uk_who' # placeholder for switching logic\n", + "REFERENCE_SET = 'uk-who' # placeholder for switching logic\n", "\n", - "print({'python': sys.version.split()[0], 'platform': platform.platform(), 'rcpchgrowth': getattr(rcpchgrowth,'__version__','unknown'), 'module_path': getattr(rcpchgrowth,'__file__','n/a'), 'pandas': pd.__version__})\n", + "print({'python': sys.version.split()[0], 'platform': platform.platform(), 'rcpchgrowth': getattr(rcpchgrowth,'__version__','unknown'), 'pandas': pd.__version__})\n", "print('Input file exists?', INPUT_CSV.exists())" ] }, @@ -75,7 +69,7 @@ "## 2. Load Input Data\n", "Expected columns (rename your fields if needed):\n", "- id (string / pseudonym)\n", - "- sex (\"male\"/\"female\")\n", + "- sex (\"M\"/\"F\")\n", "- dob (YYYY-MM-DD)\n", "- measurement_date (YYYY-MM-DD)\n", "- weight_kg (optional if computing weight SDS)\n", @@ -97,8 +91,8 @@ " # Create demo frame (remove in production)\n", " from datetime import date\n", " df = pd.DataFrame([\n", - " {'id':'A','sex':'female','dob':'2022-06-15','measurement_date':'2024-02-01','weight_kg':12.3,'height_cm':87.2},\n", - " {'id':'B','sex':'male','dob':'2021-11-10','measurement_date':'2024-02-01','weight_kg':15.0,'height_cm':93.4},\n", + " {'id':'A','sex':'F','dob':'2022-06-15','measurement_date':'2024-02-01','weight_kg':12.3,'height_cm':87.2},\n", + " {'id':'B','sex':'M','dob':'2021-11-10','measurement_date':'2024-02-01','weight_kg':15.0,'height_cm':93.4},\n", " ])\n", " df['dob'] = pd.to_datetime(df['dob'])\n", " df['measurement_date'] = pd.to_datetime(df['measurement_date'])\n", @@ -177,15 +171,15 @@ "metadata": {}, "outputs": [], "source": [ - "from rcpchgrowth import Measurement\n", + "from rcpchgrowth import sds_for_measurement, centile, select_reference_data_for_uk_who_chart\n", "\n", "def compute_sds(row, measurement_name, value_col):\n", " val = row.get(value_col)\n", " if pd.isna(val):\n", " return pd.Series({f'{measurement_name}_sds': pd.NA, f'{measurement_name}_centile': pd.NA})\n", - " measurement = Measurement(sex=row.sex, birth_date=row.dob, measurement_method=measurement_name, observation_date=row.measurement_date, observation_value=val, reference='uk-who', gestation_weeks=40, gestation_days=0).measurement\n", - " sds_val = measurement.measurement_calculated_values.corrected_sds\n", - " return pd.Series({f'{measurement_name}_sds': sds_val, f'{measurement_name}_centile': measurement.measurement_calculated_values.corrected_centile})\n", + " ref = select_reference_data_for_uk_who_chart(measurement_name, row.sex, row.age_decimal_years)\n", + " sds_val = sds_for_measurement(measurement_name, val, ref)\n", + " return pd.Series({f'{measurement_name}_sds': sds_val, f'{measurement_name}_centile': centile(sds_val)})\n", "\n", "for m, col in [('weight','weight_kg'), ('height','height_cm')]:\n", " res = df.apply(lambda r: compute_sds(r, m, col), axis=1)\n", diff --git a/pyproject.toml b/pyproject.toml index 5406c64..cceadca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "rcpchgrowth" -version = "4.3.8" +version = "4.4.0" description = "SDS and Centile calculations for UK Growth Data" keywords = ["growth charts", "anthropometry", "SDS", "centile", "UK-WHO", "UK90", "Trisomy 21", "Turner", "CDC"] license = "AGPL-3.0-or-later" From dfada66937cf7b635fa1662b982466c6ef76958c Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Fri, 15 Aug 2025 23:15:00 +0100 Subject: [PATCH 07/11] imports velocity and acceleration directly, adds two new notebooks --- notebooks/AdditionalFunctions.ipynb | 223 ++++++++++++++++++++++++++ notebooks/ExperimentalFunctions.ipynb | 133 +++++++++++++++ rcpchgrowth/__init__.py | 2 +- s/up | 2 +- 4 files changed, 358 insertions(+), 2 deletions(-) create mode 100644 notebooks/AdditionalFunctions.ipynb create mode 100644 notebooks/ExperimentalFunctions.ipynb diff --git a/notebooks/AdditionalFunctions.ipynb b/notebooks/AdditionalFunctions.ipynb new file mode 100644 index 0000000..a26350d --- /dev/null +++ b/notebooks/AdditionalFunctions.ipynb @@ -0,0 +1,223 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e9fef332", + "metadata": {}, + "source": [ + "# Additional Functions\n", + "\n", + "The core RCPCHGrowth package functions are accessed through the `Measurement` class. The functions that make up the calculations that generate the `Measurement` class object though can be accessed independently as well some extras. \n", + "\n", + "## Date Calculations\n", + "\n", + "There are four functions that can be leveraged:\n", + "\n", + "`chronological_decimal_age`\n", + "`corrected_decimal_age`\n", + "`chronological_calendar_age`\n", + "`estimated_date_delivery`\n", + "`corrected_gestational_age`" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "1e264c33", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Chronological Age (Decimal): 0.047 years\n", + "Corrected Age (Decimal): -0.230 years\n", + "Estimated Date of Delivery: 2020-04-11\n", + "Corrected Gestational Age: 28 + 0 days\n", + "Chronological Age (Calendar): 2 weeks and 3 days\n" + ] + } + ], + "source": [ + "# python imports\n", + "from datetime import date\n", + "import sys, platform, pathlib\n", + "import importlib\n", + "\n", + "# RCPCHGrowth imports - initial set up\n", + "try:\n", + " import rcpchgrowth # already installed (editable or site package)\n", + "except ImportError:\n", + " # Walk up from CWD to find a directory containing pyproject.toml AND rcpchgrowth/__init__.py\n", + " start = pathlib.Path.cwd()\n", + " repo_root = None\n", + " for cand in [start, *start.parents]:\n", + " if (cand / \"pyproject.toml\").is_file() and (cand / \"rcpchgrowth\" / \"__init__.py\").is_file():\n", + " repo_root = cand\n", + " break\n", + " if repo_root:\n", + " if str(repo_root) not in sys.path:\n", + " sys.path.insert(0, str(repo_root))\n", + " rcpchgrowth = importlib.import_module(\"rcpchgrowth\")\n", + " print(f\"Loaded rcpchgrowth from source tree: {rcpchgrowth.__file__}\")\n", + " else:\n", + " raise ImportError(\"RCPCHGrowth package not found (no pyproject.toml + rcpchgrowth/ up the directory tree).\")\n", + "\n", + "\n", + "birth_date = date(2020, 1, 1)\n", + "observation_date = date(2020, 1, 18)\n", + "gestation_weeks = 25\n", + "gestation_days = 4\n", + "\n", + "chronological_age = rcpchgrowth.chronological_decimal_age(birth_date=birth_date, observation_date=observation_date)\n", + "corrected_age = rcpchgrowth.corrected_decimal_age(birth_date=birth_date, observation_date=observation_date, gestation_weeks=gestation_weeks, gestation_days=gestation_days)\n", + "edd = rcpchgrowth.estimated_date_delivery(birth_date=birth_date, gestation_weeks=gestation_weeks, gestation_days=gestation_days)\n", + "cga = rcpchgrowth.corrected_gestational_age(birth_date=birth_date, observation_date=observation_date, gestation_weeks=gestation_weeks, gestation_days=gestation_days)\n", + "chronological_age_calendar = rcpchgrowth.chronological_calendar_age(birth_date=birth_date, observation_date=observation_date)\n", + "\n", + "print(f\"Chronological Age (Decimal): {chronological_age:.3f} years\")\n", + "print(f\"Corrected Age (Decimal): {corrected_age:.3f} years\")\n", + "print(f\"Estimated Date of Delivery: {edd}\")\n", + "print(f\"Corrected Gestational Age: {cga.get('corrected_gestation_weeks')} + {cga.get('corrected_gestation_days')} days\")\n", + "print(f\"Chronological Age (Calendar): {chronological_age_calendar}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "4a55b99c", + "metadata": {}, + "source": [ + "## BMI Functions\n", + "\n", + "These functions calculate BMI but also include % median BMI\n", + "\n", + "`bmi_from_height_weight`\n", + "`weight_for_bmi_height`\n", + "`percentage_median_bmi`" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "6d0f8174", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BMI: 17.15 kg/m²\n", + "Percentage Median BMI: 88.63%\n", + "Target weight for BMI 24: 62.99 kg\n" + ] + } + ], + "source": [ + "# python imports\n", + "from datetime import date\n", + "\n", + "# RCPCHGrowth imports\n", + "import rcpchgrowth\n", + "\n", + "birth_date = date(2010, 1, 1)\n", + "observation_date = date(2025, 1, 18)\n", + "height = 162\n", + "weight = 45\n", + "\n", + "# Calculate BMI\n", + "bmi = rcpchgrowth.bmi_from_height_weight(height=height, weight=weight)\n", + "\n", + "# Calculate age\n", + "age = rcpchgrowth.chronological_decimal_age(birth_date=birth_date, observation_date=observation_date)\n", + "\n", + "print(f\"BMI: {bmi:.2f} kg/m²\")\n", + "\n", + "# Calculate percentage median BMI\n", + "pct_median_bmi = rcpchgrowth.percentage_median_bmi(age=age, actual_bmi=bmi, sex=\"male\", reference=\"uk-who\")\n", + "print(f\"Percentage Median BMI: {pct_median_bmi:.2f}%\")\n", + "\n", + "# Calculate the weight for a given height and BMI\n", + "target_bmi = 24\n", + "target_weight = rcpchgrowth.weight_for_bmi_height(bmi=target_bmi, height=height)\n", + "print(f\"Target weight for BMI {target_bmi}: {target_weight:.2f} kg\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "ed611448", + "metadata": {}, + "source": [ + "## Midparental Height Calculations\n", + "\n", + "This calculates midparental height from parental heights. There are different methods and these are all supported.\n", + "\n", + "Most paediatricians learn that midparental height is the average of the corrected height of the parents, where height is corrected for sex. It is reasoned that the average difference in height between men and women is 13 cm, therefore this is added to the height of the mother for a boy, or subtracted from the height of the father for a girl.\n", + "\n", + "Published studies though show that this method is vulnerable to the 'regression to the mean' observation, where outliers (extremely tall or short people) tend to have children that are closer to the mean. It is this method that is endorsed by the RCPCH. The calculation here is to take the mean of the parental sds values and then apply a regression constant of 0.5. The final calculation is: (MatHtz +PatHtz)/4\n", + "\n", + "| The strengths and limitations of parental heights as a predictor of attained height, Charlotte M Wright, Tim D Cheetham, Arch Dis Child 1999;81:257–260" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "d53147e3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Standard Mid-Parental Height: 179.00 cm\n", + "RCPCH Mid-Parental Height (Z-score): 0.15\n", + "RCPCH Mid-Parental Height: 178.40 cm\n", + "Lower SDS Threshold (-1.4 SD): 168.64 cm\n", + "Upper SDS Threshold (+1.4 SD): 188.15 cm\n" + ] + } + ], + "source": [ + "from rcpchgrowth import *\n", + "\n", + "paternal_height = 180\n", + "maternal_height = 165\n", + "\n", + "standard_mid_parental_height = mid_parental_height(paternal_height=paternal_height, maternal_height=maternal_height, sex=\"male\")\n", + "\n", + "rcpch_mid_parental_height_z = mid_parental_height_z(paternal_height=paternal_height, maternal_height=maternal_height, reference=\"uk-who\")\n", + "lower_threshold_z, upper_threshold_z = lower_and_upper_limits_of_expected_height_z(mid_parental_height_z=rcpch_mid_parental_height_z)\n", + "\n", + "rcpch_mid_parental_height = measurement_from_sds(reference=\"uk-who\", requested_sds=rcpch_mid_parental_height_z, age=20, sex=\"male\", measurement_method=\"height\")\n", + "lower_threshold = measurement_from_sds(reference=\"uk-who\", requested_sds=lower_threshold_z, age=20, sex=\"male\", measurement_method=\"height\")\n", + "upper_threshold = measurement_from_sds(reference=\"uk-who\", requested_sds=upper_threshold_z, age=20, sex=\"male\", measurement_method=\"height\")\n", + "\n", + "print(f\"Standard Mid-Parental Height: {standard_mid_parental_height:.2f} cm\")\n", + "print(f\"RCPCH Mid-Parental Height (Z-score): {rcpch_mid_parental_height_z:.2f}\")\n", + "print(f\"RCPCH Mid-Parental Height: {rcpch_mid_parental_height:.2f} cm\")\n", + "print(f\"Lower SDS Threshold (-1.4 SD): {lower_threshold:.2f} cm\")\n", + "print(f\"Upper SDS Threshold (+1.4 SD): {upper_threshold:.2f} cm\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/ExperimentalFunctions.ipynb b/notebooks/ExperimentalFunctions.ipynb new file mode 100644 index 0000000..6e576e6 --- /dev/null +++ b/notebooks/ExperimentalFunctions.ipynb @@ -0,0 +1,133 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "eafcff69", + "metadata": {}, + "source": [ + "# Experimental Functions\n", + "\n", + "There are a few functions currently still under development that can be tried here.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "2aa4fa4f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded rcpchgrowth from installed package: /Users/eatyourpeas/Development/RCPCH repositories/growth charts/rcpchgrowth-python/rcpchgrowth/__init__.py\n" + ] + } + ], + "source": [ + "# start by setting up\n", + "\n", + "\n", + "from datetime import date\n", + "import sys, platform, pathlib\n", + "import importlib\n", + "\n", + "# RCPCHGrowth imports - initial set up\n", + "try:\n", + " import rcpchgrowth # already installed (editable or site package)\n", + " print(f\"Loaded rcpchgrowth from installed package: {rcpchgrowth.__file__}\")\n", + "except ImportError:\n", + " # Walk up from CWD to find a directory containing pyproject.toml AND rcpchgrowth/__init__.py\n", + " start = pathlib.Path.cwd()\n", + " repo_root = None\n", + " for cand in [start, *start.parents]:\n", + " if (cand / \"pyproject.toml\").is_file() and (cand / \"rcpchgrowth\" / \"__init__.py\").is_file():\n", + " repo_root = cand\n", + " print(f\"Found RCPCHGrowth package at: {repo_root}\")\n", + " break\n", + " if repo_root:\n", + " if str(repo_root) not in sys.path:\n", + " sys.path.insert(0, str(repo_root))\n", + " rcpchgrowth = importlib.import_module(\"rcpchgrowth\")\n", + " print(f\"Loaded rcpchgrowth from source tree: {rcpchgrowth.__file__}\")\n", + " else:\n", + " raise ImportError(\"RCPCHGrowth package not found (no pyproject.toml + rcpchgrowth/ up the directory tree).\")" + ] + }, + { + "cell_type": "markdown", + "id": "288398e4", + "metadata": {}, + "source": [ + "## Dynamic Growth \n", + "\n", + "Height, weight, BMI or OFC in terms of SDS / Centile are snapshots in time and tell\n", + "us actually very little about growth, which is a dynamic measure. In order to make\n", + "predictions, we need to look at change in parameter measured over time (velocity)\n", + "which requires 2 measurements over a known time interval, or change in velocity (acceleration/deceleration)\n", + "which requires three measurements.\n", + "\n", + "From these measurements predictions can be made about speed of growth, or rate of slowing (catch down) or acceleration (catch up). \n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "d0315caa", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "height velocity: 6.00 cm/y\n", + "height acceleration: -0.37 cm/y²\n" + ] + } + ], + "source": [ + "# python imports\n", + "from datetime import date\n", + "\n", + "# RCPCHGrowth imports\n", + "from rcpchgrowth.dynamic_growth import *\n", + "\n", + "# first generate two measurements\n", + "\n", + "first_height_measurement = Measurement(reference=\"uk-who\", sex=\"female\", measurement_method=\"height\", birth_date=date(2020, 11, 2), observation_date=date(2022,5,3), observation_value=75.1).measurement\n", + "second_height_measurement = Measurement(reference=\"uk-who\", sex=\"female\", measurement_method=\"height\", birth_date=date(2020, 11, 2), observation_date=date(2023,5,3), observation_value=81.5).measurement\n", + "third_height_measurement = Measurement(reference=\"uk-who\", sex=\"female\", measurement_method=\"height\", birth_date=date(2020, 11, 2), observation_date=date(2024,6,8), observation_value=88.1).measurement\n", + "\n", + "measurement_array = [first_height_measurement, second_height_measurement, third_height_measurement]\n", + "\n", + "height_velocity = velocity(parameter=\"height\", measurements_array=measurement_array)\n", + "\n", + "height_acceleration = acceleration(parameter=\"height\", measurements_array=measurement_array)\n", + "\n", + "print(f\"height velocity: {height_velocity:.2f} cm/y\")\n", + "print(f\"height acceleration: {height_acceleration:.2f} cm/y²\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/rcpchgrowth/__init__.py b/rcpchgrowth/__init__.py index b91cf37..21b2e89 100644 --- a/rcpchgrowth/__init__.py +++ b/rcpchgrowth/__init__.py @@ -5,7 +5,7 @@ from .chart_functions import create_chart from .constants import * # noqa: F401,F403 from .date_calculations import chronological_decimal_age, corrected_decimal_age, chronological_calendar_age, estimated_date_delivery, corrected_gestational_age -from .dynamic_growth import create_thrive_line, return_correlation, create_thrive_lines +from .dynamic_growth import create_thrive_line, return_correlation, create_thrive_lines, velocity, acceleration from .global_functions import centile, sds_for_measurement, measurement_from_sds, percentage_median_bmi, measurement_for_z, cubic_interpolation, linear_interpolation from .fictional_child import generate_fictional_child_data from .measurement import Measurement diff --git a/s/up b/s/up index 203803b..76424ca 100755 --- a/s/up +++ b/s/up @@ -6,5 +6,5 @@ set -e # Exit on error # scripts may need to be made executable on some platforms before they can be run # chmod +x is the command to do this on unixy systems -# starts all docker compose services +# starts all docker compose services - will create a notebooks container and a development container docker compose up \ No newline at end of file From 3c3aad118433d4a67b59d07707e6b7e8261075c9 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Sat, 16 Aug 2025 08:24:09 +0100 Subject: [PATCH 08/11] fix imports in notebooks --- README.md | 12 +++++--- notebooks/ResearchTemplate.ipynb | 52 +++++++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e5f0a06..e5862d9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -# RCPCH Digital Growth Python library +# RCPCHGrowth Python library -[![PyPI version](https://img.shields.io/pypi/v/rcpchgrowth.svg)](https://pypi.org/project/rcpchgrowth/) -[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) -[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/rcpch/rcpchgrowth-python/live?labpath=notebooks%2FQuickstart.ipynb) -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/rcpch/rcpchgrowth-python?quickstart=1) +## Calculations for children's measurements against UK and international growth references. + +[![PyPI version](https://img.shields.io/pypi/v/rcpchgrowth.svg?style=flat-square&labelColor=%2311a7f2&color=%230d0d58)](https://pypi.org/project/rcpchgrowth/) +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg?style=flat-square&labelColor=%2311a7f2&color=%230d0d58)](https://www.gnu.org/licenses/agpl-3.0) +[![Binder](https://img.shields.io/badge/Binder-?style=flat-square&labelColor=%2311a7f2&color=%230d0d58&logo=binder&logoColor=white)](https://mybinder.org/v2/gh/rcpch/rcpchgrowth-python/live?labpath=notebooks%2FQuickstart.ipynb) +[![Codespaces](https://img.shields.io/badge/Codespaces-Open_in_Cloud?style=flat-square&labelColor=%2311a7f2&color=%230d0d58&logo=github&logoColor=white)](https://codespaces.new/rcpch/rcpchgrowth-python?quickstart=1) Please go to for full documentation. diff --git a/notebooks/ResearchTemplate.ipynb b/notebooks/ResearchTemplate.ipynb index 0cea1b2..6d67930 100644 --- a/notebooks/ResearchTemplate.ipynb +++ b/notebooks/ResearchTemplate.ipynb @@ -42,22 +42,50 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "64bbcd08", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded rcpchgrowth from installed package: /Users/eatyourpeas/Development/RCPCH repositories/growth charts/rcpchgrowth-python/rcpchgrowth/__init__.py\n", + "Input file exists? False\n" + ] + } + ], "source": [ "from pathlib import Path\n", + "import importlib\n", "import sys, platform\n", "import pandas as pd\n", - "import rcpchgrowth\n", "from datetime import date\n", "\n", "INPUT_CSV = Path('YOUR_INPUT_FILE.csv') # <-- change\n", "OUTPUT_CSV = Path('augmented_results.csv')\n", "REFERENCE_SET = 'uk-who' # placeholder for switching logic\n", "\n", - "print({'python': sys.version.split()[0], 'platform': platform.platform(), 'rcpchgrowth': getattr(rcpchgrowth,'__version__','unknown'), 'pandas': pd.__version__})\n", + "# RCPCHGrowth imports - initial set up\n", + "try:\n", + " import rcpchgrowth # already installed (editable or site package)\n", + " print(f\"Loaded rcpchgrowth from installed package: {rcpchgrowth.__file__}\")\n", + "except ImportError:\n", + " # Walk up from CWD to find a directory containing pyproject.toml AND rcpchgrowth/__init__.py\n", + " start = pathlib.Path.cwd()\n", + " repo_root = None\n", + " for cand in [start, *start.parents]:\n", + " if (cand / \"pyproject.toml\").is_file() and (cand / \"rcpchgrowth\" / \"__init__.py\").is_file():\n", + " repo_root = cand\n", + " print(f\"Found RCPCHGrowth package at: {repo_root}\")\n", + " break\n", + " if repo_root:\n", + " if str(repo_root) not in sys.path:\n", + " sys.path.insert(0, str(repo_root))\n", + " rcpchgrowth = importlib.import_module(\"rcpchgrowth\")\n", + " print(f\"Loaded rcpchgrowth from source tree: {rcpchgrowth.__file__}\")\n", + " else:\n", + " raise ImportError(\"RCPCHGrowth package not found (no pyproject.toml + rcpchgrowth/ up the directory tree).\")\n", "print('Input file exists?', INPUT_CSV.exists())" ] }, @@ -272,8 +300,22 @@ } ], "metadata": { + "kernelspec": { + "display_name": "rcpchgrowth", + "language": "python", + "name": "python3" + }, "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.0" } }, "nbformat": 4, From bc0fe15a6040298df383eb2d8ece971067a40e12 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Sat, 16 Aug 2025 08:24:31 +0100 Subject: [PATCH 09/11] remove bump2version and _version.py --- .bumpversion.cfg | 8 -------- rcpchgrowth/__init__.py | 1 - rcpchgrowth/_version.py | 11 ----------- 3 files changed, 20 deletions(-) delete mode 100644 .bumpversion.cfg delete mode 100644 rcpchgrowth/_version.py diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index c19c1c5..0000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[bumpversion] -current_version = 4.3.8 -commit = True -tag = True -tag_name = v{new_version} -message = chore: bump version {current_version} → {new_version} - -[bumpversion:file:VERSION] diff --git a/rcpchgrowth/__init__.py b/rcpchgrowth/__init__.py index 21b2e89..82e092a 100644 --- a/rcpchgrowth/__init__.py +++ b/rcpchgrowth/__init__.py @@ -14,4 +14,3 @@ from .trisomy_21_aap import select_reference_data_for_trisomy_21_aap from .turner import select_reference_data_for_turner from .uk_who import select_reference_data_for_uk_who_chart -from ._version import __version__ # single-source version diff --git a/rcpchgrowth/_version.py b/rcpchgrowth/_version.py deleted file mode 100644 index 0e8b5cd..0000000 --- a/rcpchgrowth/_version.py +++ /dev/null @@ -1,11 +0,0 @@ -__all__ = ["__version__"] - -try: - from importlib.metadata import version as _meta_version -except ImportError: # Python <3.8 fallback not needed, but kept minimal - from importlib_metadata import version as _meta_version # type: ignore - -try: - __version__ = _meta_version("rcpchgrowth") -except Exception: - __version__ = "0.0.0+unknown" From 10f4c29df5ec4d2ad7f80967deaedc39dcecc8a1 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Sat, 16 Aug 2025 10:12:33 +0100 Subject: [PATCH 10/11] rejigs docker compose to create only one container (dev) which launches jupyter in browser with hot reload. pulls version number from pyproject.yaml via extra snippet in `__init__.py` Tidies the initialization steps in the notebooks to be less complicated and verbose --- Dockerfile | 1 - docker-compose.yml | 37 ++++++++++++++------------- notebooks/AdditionalFunctions.ipynb | 30 ++++++---------------- notebooks/ExperimentalFunctions.ipynb | 33 +++++------------------- notebooks/Quickstart.ipynb | 33 +++++------------------- notebooks/ResearchTemplate.ipynb | 30 +++++----------------- rcpchgrowth/__init__.py | 22 ++++++++++++++++ 7 files changed, 69 insertions(+), 117 deletions(-) diff --git a/Dockerfile b/Dockerfile index 590d6a0..5ca8096 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ # Copy metadata first for build cache COPY pyproject.toml README.md ./ COPY requirements.txt ./ -COPY rcpchgrowth/_version.py rcpchgrowth/_version.py # Copy the full source (needed for editable install) COPY rcpchgrowth rcpchgrowth diff --git a/docker-compose.yml b/docker-compose.yml index 27c7608..b721bf6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,26 +1,27 @@ -version: "3.12" - +version: "3.9" services: - rcpchgrowth-dev: + dev: build: . container_name: rcpchgrowth-dev volumes: - .:/app working_dir: /app - command: bash - tty: true - stdin_open: true - - rcpchgrowth-notebooks: - build: . - container_name: rcpchgrowth-notebooks - volumes: - - .:/app - working_dir: /app/notebooks - ports: - - "8888:8888" - command: bash -c "jupyter lab --ip=0.0.0.0 --no-browser --NotebookApp.token='' --NotebookApp.password='' --allow-root" environment: - PYTHONPATH=/app - depends_on: - - rcpchgrowth-dev + ports: + - "8888:8888" + command: + - bash + - -lc + - | + set -e + mkdir -p /app/notebooks + echo '[DEV] Editable install' + pip install -e /app[notebook] + echo '[DEV] Sanity import' + echo '[DEV] Register kernel' + python -m ipykernel install --name rcpchgrowth --display-name 'rcpchgrowth' --sys-prefix + echo '[DEV] Launching JupyterLab' + exec jupyter lab --ip=0.0.0.0 --no-browser --allow-root \ + --NotebookApp.token='' --NotebookApp.password='' \ + --notebook-dir=/app/notebooks diff --git a/notebooks/AdditionalFunctions.ipynb b/notebooks/AdditionalFunctions.ipynb index a26350d..60c746f 100644 --- a/notebooks/AdditionalFunctions.ipynb +++ b/notebooks/AdditionalFunctions.ipynb @@ -22,7 +22,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 1, "id": "1e264c33", "metadata": {}, "outputs": [ @@ -30,6 +30,7 @@ "name": "stdout", "output_type": "stream", "text": [ + "rcpchgrowth 4.4.0 | Python 3.12.11\n", "Chronological Age (Decimal): 0.047 years\n", "Corrected Age (Decimal): -0.230 years\n", "Estimated Date of Delivery: 2020-04-11\n", @@ -41,27 +42,12 @@ "source": [ "# python imports\n", "from datetime import date\n", - "import sys, platform, pathlib\n", - "import importlib\n", - "\n", - "# RCPCHGrowth imports - initial set up\n", - "try:\n", - " import rcpchgrowth # already installed (editable or site package)\n", - "except ImportError:\n", - " # Walk up from CWD to find a directory containing pyproject.toml AND rcpchgrowth/__init__.py\n", - " start = pathlib.Path.cwd()\n", - " repo_root = None\n", - " for cand in [start, *start.parents]:\n", - " if (cand / \"pyproject.toml\").is_file() and (cand / \"rcpchgrowth\" / \"__init__.py\").is_file():\n", - " repo_root = cand\n", - " break\n", - " if repo_root:\n", - " if str(repo_root) not in sys.path:\n", - " sys.path.insert(0, str(repo_root))\n", - " rcpchgrowth = importlib.import_module(\"rcpchgrowth\")\n", - " print(f\"Loaded rcpchgrowth from source tree: {rcpchgrowth.__file__}\")\n", - " else:\n", - " raise ImportError(\"RCPCHGrowth package not found (no pyproject.toml + rcpchgrowth/ up the directory tree).\")\n", + "import rcpchgrowth, sys\n", + "\n", + "# set up\n", + "print(f\"rcpchgrowth {rcpchgrowth.__version__} | Python {sys.version.split()[0]}\")\n", + "%load_ext autoreload\n", + "%autoreload 2\n", "\n", "\n", "birth_date = date(2020, 1, 1)\n", diff --git a/notebooks/ExperimentalFunctions.ipynb b/notebooks/ExperimentalFunctions.ipynb index 6e576e6..6943e14 100644 --- a/notebooks/ExperimentalFunctions.ipynb +++ b/notebooks/ExperimentalFunctions.ipynb @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 1, "id": "2aa4fa4f", "metadata": {}, "outputs": [ @@ -20,38 +20,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "Loaded rcpchgrowth from installed package: /Users/eatyourpeas/Development/RCPCH repositories/growth charts/rcpchgrowth-python/rcpchgrowth/__init__.py\n" + "rcpchgrowth 4.4.0 | Python 3.12.11\n" ] } ], "source": [ "# start by setting up\n", "\n", - "\n", - "from datetime import date\n", - "import sys, platform, pathlib\n", - "import importlib\n", - "\n", - "# RCPCHGrowth imports - initial set up\n", - "try:\n", - " import rcpchgrowth # already installed (editable or site package)\n", - " print(f\"Loaded rcpchgrowth from installed package: {rcpchgrowth.__file__}\")\n", - "except ImportError:\n", - " # Walk up from CWD to find a directory containing pyproject.toml AND rcpchgrowth/__init__.py\n", - " start = pathlib.Path.cwd()\n", - " repo_root = None\n", - " for cand in [start, *start.parents]:\n", - " if (cand / \"pyproject.toml\").is_file() and (cand / \"rcpchgrowth\" / \"__init__.py\").is_file():\n", - " repo_root = cand\n", - " print(f\"Found RCPCHGrowth package at: {repo_root}\")\n", - " break\n", - " if repo_root:\n", - " if str(repo_root) not in sys.path:\n", - " sys.path.insert(0, str(repo_root))\n", - " rcpchgrowth = importlib.import_module(\"rcpchgrowth\")\n", - " print(f\"Loaded rcpchgrowth from source tree: {rcpchgrowth.__file__}\")\n", - " else:\n", - " raise ImportError(\"RCPCHGrowth package not found (no pyproject.toml + rcpchgrowth/ up the directory tree).\")" + "import rcpchgrowth, sys\n", + "print(f\"rcpchgrowth {rcpchgrowth.__version__} | Python {sys.version.split()[0]}\")\n", + "%load_ext autoreload\n", + "%autoreload 2" ] }, { diff --git a/notebooks/Quickstart.ipynb b/notebooks/Quickstart.ipynb index 690b397..e91bf47 100644 --- a/notebooks/Quickstart.ipynb +++ b/notebooks/Quickstart.ipynb @@ -22,7 +22,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 1, "id": "ae9a9912", "metadata": {}, "outputs": [ @@ -30,34 +30,15 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'python': '3.12.0', 'platform': 'macOS-15.6-arm64-arm-64bit', 'rcpchgrowth': 'unknown', 'rcpchgrowth_module_path': '/Users/eatyourpeas/Development/RCPCH repositories/growth charts/rcpchgrowth-python/rcpchgrowth/__init__.py', 'pandas': '2.3.1'}\n" + "rcpchgrowth 4.4.0 | Python 3.12.11\n" ] } ], "source": [ - "# Environment & versions (works whether installed via pip or run from cloned repo)\n", - "import sys, platform, pathlib\n", - "import pandas as pd\n", - "\n", - "try:\n", - " import rcpchgrowth\n", - "except ImportError:\n", - " import sys, pathlib\n", - " repo_root = pathlib.Path(__file__).resolve().parents[1] # notebooks/ -> project root\n", - " # Heuristic: does the package directory exist inside this root?\n", - " if (repo_root / \"rcpchgrowth\" / \"__init__.py\").is_file() and (repo_root / \"pyproject.toml\").is_file():\n", - " sys.path.insert(0, str(repo_root))\n", - " import rcpchgrowth\n", - " else:\n", - " raise ImportError(\"RCPCHGrowth package not found\")\n", - "\n", - "print({\n", - " 'python': sys.version.split()[0],\n", - " 'platform': platform.platform(),\n", - " 'rcpchgrowth': getattr(rcpchgrowth, '__version__', 'unknown'),\n", - " 'rcpchgrowth_module_path': getattr(rcpchgrowth, '__file__', 'n/a'),\n", - " 'pandas': pd.__version__,\n", - "})" + "import rcpchgrowth, sys, platform\n", + "print(f\"rcpchgrowth {rcpchgrowth.__version__} | Python {sys.version.split()[0]}\")\n", + "%load_ext autoreload\n", + "%autoreload 2" ] }, { @@ -190,7 +171,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 1, "id": "7acb21cb", "metadata": {}, "outputs": [ diff --git a/notebooks/ResearchTemplate.ipynb b/notebooks/ResearchTemplate.ipynb index 6d67930..d400e06 100644 --- a/notebooks/ResearchTemplate.ipynb +++ b/notebooks/ResearchTemplate.ipynb @@ -42,7 +42,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "64bbcd08", "metadata": {}, "outputs": [ @@ -67,26 +67,10 @@ "REFERENCE_SET = 'uk-who' # placeholder for switching logic\n", "\n", "# RCPCHGrowth imports - initial set up\n", - "try:\n", - " import rcpchgrowth # already installed (editable or site package)\n", - " print(f\"Loaded rcpchgrowth from installed package: {rcpchgrowth.__file__}\")\n", - "except ImportError:\n", - " # Walk up from CWD to find a directory containing pyproject.toml AND rcpchgrowth/__init__.py\n", - " start = pathlib.Path.cwd()\n", - " repo_root = None\n", - " for cand in [start, *start.parents]:\n", - " if (cand / \"pyproject.toml\").is_file() and (cand / \"rcpchgrowth\" / \"__init__.py\").is_file():\n", - " repo_root = cand\n", - " print(f\"Found RCPCHGrowth package at: {repo_root}\")\n", - " break\n", - " if repo_root:\n", - " if str(repo_root) not in sys.path:\n", - " sys.path.insert(0, str(repo_root))\n", - " rcpchgrowth = importlib.import_module(\"rcpchgrowth\")\n", - " print(f\"Loaded rcpchgrowth from source tree: {rcpchgrowth.__file__}\")\n", - " else:\n", - " raise ImportError(\"RCPCHGrowth package not found (no pyproject.toml + rcpchgrowth/ up the directory tree).\")\n", - "print('Input file exists?', INPUT_CSV.exists())" + "import rcpchgrowth, sys, platform\n", + "print(f\"RCPCHGrowth {rcpchgrowth.__version__} | Python {sys.version.split()[0]}\")\n", + "%load_ext autoreload\n", + "%autoreload 2" ] }, { @@ -301,7 +285,7 @@ ], "metadata": { "kernelspec": { - "display_name": "rcpchgrowth", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -315,7 +299,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.0" + "version": "3.12.11" } }, "nbformat": 4, diff --git a/rcpchgrowth/__init__.py b/rcpchgrowth/__init__.py index 82e092a..c84b118 100644 --- a/rcpchgrowth/__init__.py +++ b/rcpchgrowth/__init__.py @@ -14,3 +14,25 @@ from .trisomy_21_aap import select_reference_data_for_trisomy_21_aap from .turner import select_reference_data_for_turner from .uk_who import select_reference_data_for_uk_who_chart + +# Version +try: + from importlib import metadata as _md + __version__ = _md.version("rcpchgrowth") +except Exception: + import pathlib, sys + _root = pathlib.Path(__file__).resolve().parent.parent + _pyproj = _root / "pyproject.toml" + _ver = "0.0.0+unknown" + if _pyproj.is_file(): + try: + if sys.version_info >= (3, 11): + import tomllib + else: + import tomli as tomllib # type: ignore + with _pyproj.open("rb") as f: + _data = tomllib.load(f) + _ver = _data.get("project", {}).get("version", _ver) + except Exception: + pass + __version__ = _ver \ No newline at end of file From 8d6f47962ae34c67da6251e2a61a8269046419d2 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Sat, 16 Aug 2025 10:41:25 +0100 Subject: [PATCH 11/11] add binder requirements and fix badges --- .binder/requirements.txt | 1 + README.md | 74 +++++++++++----------------------------- 2 files changed, 21 insertions(+), 54 deletions(-) create mode 100644 .binder/requirements.txt diff --git a/.binder/requirements.txt b/.binder/requirements.txt new file mode 100644 index 0000000..2853f1a --- /dev/null +++ b/.binder/requirements.txt @@ -0,0 +1 @@ +e .[notebook] \ No newline at end of file diff --git a/README.md b/README.md index e5862d9..ae46bce 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![PyPI version](https://img.shields.io/pypi/v/rcpchgrowth.svg?style=flat-square&labelColor=%2311a7f2&color=%230d0d58)](https://pypi.org/project/rcpchgrowth/) [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg?style=flat-square&labelColor=%2311a7f2&color=%230d0d58)](https://www.gnu.org/licenses/agpl-3.0) -[![Binder](https://img.shields.io/badge/Binder-?style=flat-square&labelColor=%2311a7f2&color=%230d0d58&logo=binder&logoColor=white)](https://mybinder.org/v2/gh/rcpch/rcpchgrowth-python/live?labpath=notebooks%2FQuickstart.ipynb) +[![Binder](https://img.shields.io/badge/Binder-Launch?style=flat-square&labelColor=%2311a7f2&color=%230d0d58&logo=binder&logoColor=white)](https://mybinder.org/v2/gh/rcpch/rcpchgrowth-python/live?labpath=notebooks%2FQuickstart.ipynb) [![Codespaces](https://img.shields.io/badge/Codespaces-Open_in_Cloud?style=flat-square&labelColor=%2311a7f2&color=%230d0d58&logo=github&logoColor=white)](https://codespaces.new/rcpch/rcpchgrowth-python?quickstart=1) Please go to for full documentation. @@ -22,28 +22,22 @@ If you want to avoid setting up docker environments, there are shortcut scripts First ensure the folders have the correct permissions: ```bash -chmod +x s/dev s/notebooks +chmod +x s/dev ``` -To generate a container with the installed package for development +To generate a container which will launch the notebooks in a browser and allow local dev ( with hot reload) ```bash s/dev ``` -To generate a container that launches the jupyter notebooks to port 8888 - -```bash -s/notebooks -``` - ### Minimal installation (assuming you have a python virtual env setup) ```bash pip install rcpchgrowth ``` -With notebook & plotting convenience dependencies: +With notebook & package dependencies: ```bash pip install "rcpchgrowth[notebook]" @@ -64,54 +58,26 @@ Example notebooks live in `notebooks/`: - `Quickstart.ipynb` – single measurement, small batch, simple plot. - `ResearchTemplate.ipynb` – structured workflow for batch CSV processing (ages, SDS, centiles, quality flags, export). - -### Launch options - -- Binder badge above opens the Quickstart notebook (branch `live`). Binder builds from this repo's `requirements.txt`; to add notebook extras inside Binder run: - - ```bash - pip install "rcpchgrowth[notebook]" - ``` - -- Codespaces badge launches a ready cloud dev environment; open the notebooks folder afterwards. +- `AdditionalFunctions.ipynb` - exposes some of the date and calculation functions for more in-depth exploration +- `ExperimentalFunctions.ipynb` - exposes functions that are in development and more experimental ## Data handling / privacy -Do NOT place identifiable patient data in a public fork or commit history. De‑identify and keep raw data outside version control. The research template includes guidance for exporting enriched results safely. - -## Basic usage (programmatic) + + + + + +
+ Data handling & privacy
+ Never commit identifiable patient data.
+ • Keep raw identifiable data outside version control (secure, access‑controlled).
+ • De‑identify before analysis (remove names, NHS numbers, full DOB; date‑shift if required).
+ • Do not push raw exports to forks, PRs or gists.
+ • Use ResearchTemplate.ipynb for generating de‑identified derived outputs.
+ If in doubt, stop and seek local information governance guidance. +
-```python -from datetime import date -from rcpchgrowth import Measurement - -sex = 'female' -dob = date(2022, 6, 15) -md = date(2024, 2, 1) -weight_kg = 12.3 - -measurement = Measurement(birth_date=dob, measurement_method='weight', observation_date=md, observation_value=weight_kg, reference='uk-who', gestation_weeks=40, gestation_days=0).measurement - -# Extracting the results from the measurement dictionary - -# Calculated ages -chronological_age_decimal_years = measurement['measurement_dates']["chronological_decimal_age"] -corrected_age_decimal_years = measurement['measurement_dates']["corrected_decimal_age"] -chronological_calendar_age = measurement['measurement_dates']["chronological_calendar_age"] # returns age as readable text in years, months, weeks and days -corrected_calendar_age = measurement['measurement_dates']["corrected_calendar_age"] # returns age as readable text in years, months, weeks and days -# This returns corrected gestational age in weeks if the baby was premature and is not yet term. -corrected_gestational_age = measurement['measurement_dates']["corrected_gestational_age"]["corrected_gestation_weeks"] -corrected_gestational_age = measurement['measurement_dates']["corrected_gestational_age"]["corrected_gestation_days"] - -# calculated SDS and centiles -corrected_weight_sds = measurement["measurement_calculated_values"]["corrected_sds"] -corrected_weight_centile = measurement["measurement_calculated_values"]["corrected_centile"] -chronological_weight_sds = measurement["measurement_calculated_values"]["chronological_sds"] -chronological_weight_centile = measurement["measurement_calculated_values"]["chronological_centile"] - -print(f"Age (decimal years): {chronological_age_decimal_years:.3f}") -print(f"Weight: {weight_kg} kg | SDS: {corrected_weight_sds:.2f} | Centile: {corrected_weight_centile:.1f}") -``` ---