diff --git a/.gitignore b/.gitignore index 3f4031f..4b28f78 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,174 @@ __pycache__ .pytest_cache .pytype -*.log \ No newline at end of file +*.log + +# vscode +.vscode/* + +# Jupyter book html +_build/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + + + diff --git a/environment.yaml b/environment.yaml index 969e011..f05fd0c 100644 --- a/environment.yaml +++ b/environment.yaml @@ -1,10 +1,14 @@ name: template-des +channels: + - conda-forge dependencies: + - black - flake8=7.1.1 - ipykernel=6.29.5 - - jinja2=3.1.4 + - jinja2=3.1.5 - joblib=1.4.2 - nbformat=5.10.4 + - nbqa - numpy=2.1.3 - pandas=2.2.3 - pip=24.3.1 @@ -16,4 +20,4 @@ dependencies: - pip: - kaleido==0.2.1 - pycodestyle_magic==0.5 - - -e .[dev] \ No newline at end of file + - -e .[dev] diff --git a/notebooks/analysis.ipynb b/notebooks/analysis.ipynb index 755d038..1d9eb88 100644 --- a/notebooks/analysis.ipynb +++ b/notebooks/analysis.ipynb @@ -19,6 +19,150 @@ "* Analysis of the spread of replication results was adapted from Tom Monks (2024) HPDM097 - Making a difference with health data (https://github.com/health-data-science-OR/stochastic_systems) (MIT License)." ] }, + { + "cell_type": "markdown", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "## Plotly \"allow a user to select KPI\" histogram\n", + "\n", + "The function `create_user_controlled_hist` creates a plotly chart that allows a user to choose which of the KPIs to view. It has optional parameter to map the simulation result variable names to a display \"friendly name\" and \"units\". Users pick a KPI from a drop down list. An optional parameter can be used to exclude certain results if they are not relevant to the modeller. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import plotly.graph_objects as go" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def create_user_controlled_hist(\n", + " results, exclude_columns=None, name_mappings=None, include_instruct=True\n", + "):\n", + " \"\"\"\n", + " Create a plotly histogram that includes a drop down list that allows a user\n", + " to select which KPI is displayed as a histogram\n", + "\n", + " Params:\n", + " -------\n", + " results: pd.DataFrame\n", + " rows = replications, cols = KPIs\n", + " exclude_columns: list, optional\n", + " List of column numbers to exclude from the dropdown list\n", + " name_mappings: dict, optional\n", + " Nested dictionary mapping column names to friendly names and units\n", + " Format: {column_name: {'friendly_name': str, 'units': str}}#\n", + " include_instruct: bool, optional\n", + " Including the instruction \"Select KPI from drop down list\" above\n", + " plot. Useful for interactive applications.\n", + "\n", + " Returns:\n", + " -------\n", + " plotly.figure\n", + "\n", + " Source:\n", + " ------\n", + " The code in this function was adapted from:\n", + " https://stackoverflow.com/questions/59406167/plotly-how-to-filter-a-pandas-\n", + " dataframe-using-a-dropdown-menu\n", + "\n", + " and\n", + "\n", + " Monks T and Harper A. Improving the usability of open health service \n", + " delivery simulation models using Python and web apps \n", + " [version 2; peer review: 3 approved]. NIHR Open Res 2023, 3:48 #\n", + " https://doi.org/10.3310/nihropenres.13467.2\n", + " \"\"\"\n", + "\n", + " # create a figure\n", + " fig = go.Figure()\n", + "\n", + " # Filter out excluded columns\n", + " if exclude_columns is None:\n", + " exclude_columns = []\n", + " included_columns = [\n", + " col\n", + " for i, col in enumerate(results.columns)\n", + " if i not in exclude_columns\n", + " ]\n", + "\n", + " # set up ONE trace\n", + " first_col = included_columns[0]\n", + " first_friendly_name = (\n", + " name_mappings[first_col][\"friendly_name\"]\n", + " if name_mappings and first_col in name_mappings\n", + " else first_col\n", + " )\n", + " first_units = (\n", + " name_mappings[first_col][\"units\"]\n", + " if name_mappings and first_col in name_mappings\n", + " else \"\"\n", + " )\n", + " first_x_title = (\n", + " f\"{first_friendly_name} ({first_units})\"\n", + " if first_units\n", + " else first_friendly_name\n", + " )\n", + "\n", + " fig.add_trace(go.Histogram(x=results[first_col]))\n", + " fig.update_xaxes(title_text=first_x_title)\n", + "\n", + " buttons = []\n", + "\n", + " # create list of drop down items - KPIs\n", + " for col in included_columns:\n", + " if name_mappings and col in name_mappings:\n", + " friendly_name = name_mappings[col][\"friendly_name\"]\n", + " units = name_mappings[col][\"units\"]\n", + " x_title = f\"{friendly_name} ({units})\" if units else friendly_name\n", + " else:\n", + " friendly_name = col\n", + " x_title = col\n", + "\n", + " buttons.append(\n", + " dict(\n", + " method=\"update\",\n", + " label=friendly_name,\n", + " args=[{\"x\": [results[col]]}, {\"xaxis\": {\"title\": x_title}}],\n", + " )\n", + " )\n", + "\n", + " # create update menu and parameters\n", + " updatemenu = []\n", + " your_menu = dict()\n", + " updatemenu.append(your_menu)\n", + "\n", + " updatemenu[0][\"buttons\"] = buttons\n", + " updatemenu[0][\"direction\"] = \"down\"\n", + " updatemenu[0][\"showactive\"] = True\n", + "\n", + " # add dropdown menus to the figure\n", + " fig.update_layout(showlegend=False, updatemenus=updatemenu)\n", + "\n", + " # Add annotation as instruction\n", + " if include_instruct:\n", + " fig.add_annotation(\n", + " text=\"Select a KPI from the drop down list\",\n", + " xref=\"paper\",\n", + " yref=\"paper\",\n", + " x=0.0,\n", + " y=1.1, # Positions the text above the plot\n", + " showarrow=False,\n", + " font=dict(size=12),\n", + " )\n", + "\n", + " return fig" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -30,20 +174,20 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ - "%load_ext pycodestyle_magic" + "# %load_ext pycodestyle_magic" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ - "%pycodestyle_on" + "# %pycodestyle_on" ] }, { @@ -55,7 +199,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -68,7 +212,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -91,7 +235,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -107,11 +251,11 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ - "output_dir = '../outputs/'" + "output_dir = \"../outputs/\"" ] }, { @@ -130,7 +274,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -147,7 +291,7 @@ "\n", "Patient-level results are a large file, so that could be compressed and saved as `.csv.gz`:\n", "\n", - "```\n", + "```python\n", "# Save file\n", "trial.patient_results_df.to_csv(\n", " os.path.join(output_dir, 'example_patient.csv.gz'),\n", @@ -162,7 +306,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -257,7 +401,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -362,12 +506,13 @@ "source": [ "display(trial.trial_results_df.head())\n", "trial.trial_results_df.to_csv(\n", - " os.path.join(output_dir, 'example_trial.csv'), index=False)" + " os.path.join(output_dir, \"example_trial.csv\"), index=False\n", + ")" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -472,86 +617,1251 @@ "source": [ "display(trial.interval_audit_df.head())\n", "trial.interval_audit_df.to_csv(\n", - " os.path.join(output_dir, 'example_interval_audit.csv'), index=False)" + " os.path.join(output_dir, \"example_interval_audit.csv\"), index=False\n", + ")" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
| \n", + " | arrivals | \n", + "mean_q_time_nurse | \n", + "mean_time_with_nurse | \n", + "mean_nurse_utilisation | \n", + "
|---|---|---|---|---|
| mean | \n", + "10776.741935 | \n", + "0.499037 | \n", + "9.978457 | \n", + "0.497670 | \n", + "
| std_dev | \n", + "115.803272 | \n", + "0.067393 | \n", + "0.115138 | \n", + "0.007524 | \n", + "
| lower_95_ci | \n", + "10734.264952 | \n", + "0.474317 | \n", + "9.936224 | \n", + "0.494910 | \n", + "
| upper_95_ci | \n", + "10819.218919 | \n", + "0.523757 | \n", + "10.020690 | \n", + "0.500430 | \n", + "
90&&i.log(\"Long binary search...\"),f-1},e.sorterAsc=function(t,e){return t-e},e.sorterDes=function(t,e){return e-t},e.distinctVals=function(t){var r,n=t.slice();for(n.sort(e.sorterAsc),r=n.length-1;r>-1&&n[r]===o;r--);for(var i,a=n[r]-n[0]||1,s=a/(r||1)/1e4,l=[],c=0;c<=r;c++){var u=n[c],h=u-i;void 0===i?(l.push(u),i=u):h>s&&(a=Math.min(a,h),l.push(u),i=u)}return{vals:l,minDiff:a}},e.roundUp=function(t,e,r){for(var n,i=0,a=e.length-1,o=0,s=r?0:1,l=r?1:0,c=r?Math.ceil:Math.floor;i0&&(n=1),r&&n)return t.sort(e)}return n?t:t.reverse()},e.findIndexOfMin=function(t,e){e=e||a;for(var r,n=1/0,i=0;i
/i;e.BR_TAG_ALL=/
/gi;var b=/(^|[\\s\"'])style\\s*=\\s*(\"([^\"]*);?\"|'([^']*);?')/i,w=/(^|[\\s\"'])href\\s*=\\s*(\"([^\"]*)\"|'([^']*)')/i,T=/(^|[\\s\"'])target\\s*=\\s*(\"([^\"\\s]*)\"|'([^'\\s]*)')/i,k=/(^|[\\s\"'])popup\\s*=\\s*(\"([\\w=,]*)\"|'([\\w=,]*)')/i;function A(t,e){if(!t)return null;var r=t.match(e),n=r&&(r[3]||r[4]);return n&&C(n)}var M=/(^|;)\\s*color:/;e.plainText=function(t,e){for(var r=void 0!==(e=e||{}).len&&-1!==e.len?e.len:1/0,n=void 0!==e.allowedTags?e.allowedTags:[\"br\"],i=t.split(v),a=[],o=\"\",s=0,l=0;l
\"+l;e.text=c}(t,o,r,c):\"log\"===u?function(t,e,r,n,a){var o=t.dtick,l=e.x,c=t.tickformat,u=\"string\"==typeof o&&o.charAt(0);if(\"never\"===a&&(a=\"\"),n&&\"L\"!==u&&(o=\"L3\",u=\"L\"),c||\"L\"===u)e.text=wt(Math.pow(10,l),t,a,n);else if(i(o)||\"D\"===u&&s.mod(l+.01,1)<.1){var h=Math.round(l),f=Math.abs(h),p=t.exponentformat;\"power\"===p||_t(p)&&bt(h)?(e.text=0===h?1:1===h?\"10\":\"10\"+(h>1?\"\":z)+f+\"\",e.fontSize*=1.25):(\"e\"===p||\"E\"===p)&&f>2?e.text=\"1\"+p+(h>0?\"+\":z)+f:(e.text=wt(Math.pow(10,l),t,\"\",\"fakehover\"),\"D1\"===o&&\"y\"===t._id.charAt(0)&&(e.dy-=e.fontSize/6))}else{if(\"D\"!==u)throw\"unrecognized dtick \"+String(o);e.text=String(Math.round(Math.pow(10,s.mod(l,1)))),e.fontSize*=.75}if(\"D1\"===t.dtick){var d=String(e.text).charAt(0);\"0\"!==d&&\"1\"!==d||(\"y\"===t._id.charAt(0)?e.dx-=e.fontSize/4:(e.dy+=e.fontSize/2,e.dx+=(t.range[1]>t.range[0]?1:-1)*e.fontSize*(l<0?.5:.25)))}}(t,o,0,c,g):\"category\"===u?function(t,e){var r=t._categories[Math.round(e.x)];void 0===r&&(r=\"\"),e.text=String(r)}(t,o):\"multicategory\"===u?function(t,e,r){var n=Math.round(e.x),i=t._categories[n]||[],a=void 0===i[1]?\"\":String(i[1]),o=void 0===i[0]?\"\":String(i[0]);r?e.text=o+\" - \"+a:(e.text=a,e.text2=o)}(t,o,r):Rt(t)?function(t,e,r,n,i){if(\"radians\"!==t.thetaunit||r)e.text=wt(e.x,t,i,n);else{var a=e.x/180;if(0===a)e.text=\"0\";else{var o=function(t){function e(t,e){return Math.abs(t-e)<=1e-6}var r=function(t){for(var r=1;!e(Math.round(t*r)/r,t);)r*=10;return r}(t),n=t*r,i=Math.abs(function t(r,n){return e(n,0)?r:t(n,r%n)}(n,r));return[Math.round(n/i),Math.round(r/i)]}(a);if(o[1]>=100)e.text=wt(s.deg2rad(e.x),t,i,n);else{var l=e.x<0;1===o[1]?1===o[0]?e.text=\"π\":e.text=o[0]+\"π\":e.text=[\"\",o[0],\"\",\"⁄\",\"\",o[1],\"\",\"π\"].join(\"\"),l&&(e.text=z+e.text)}}}}(t,o,r,c,g):function(t,e,r,n,i){\"never\"===i?i=\"\":\"all\"===t.showexponent&&Math.abs(e.x/t.dtick)<1e-6&&(i=\"hide\"),e.text=wt(e.x,t,i,n)}(t,o,0,c,g),n||(t.tickprefix&&!m(t.showtickprefix)&&(o.text=t.tickprefix+o.text),t.ticksuffix&&!m(t.showticksuffix)&&(o.text+=t.ticksuffix)),t.labelalias&&t.labelalias.hasOwnProperty(o.text)){var y=t.labelalias[o.text];\"string\"==typeof y&&(o.text=y)}return(\"boundaries\"===t.tickson||t.showdividers)&&(o.xbnd=[f(o.x-.5),f(o.x+t.dtick-.5)]),o},Z.hoverLabelText=function(t,e,r){r&&(t=s.extendFlat({},t,{hoverformat:r}));var n=s.isArrayOrTypedArray(e)?e[0]:e,i=s.isArrayOrTypedArray(e)?e[1]:void 0;if(void 0!==i&&i!==n)return Z.hoverLabelText(t,n,r)+\" - \"+Z.hoverLabelText(t,i,r);var a=\"log\"===t.type&&n<=0,o=Z.tickText(t,t.c2l(a?-n:n),\"hover\").text;return a?0===n?\"0\":z+o:o};var xt=[\"f\",\"p\",\"n\",\"μ\",\"m\",\"\",\"k\",\"M\",\"G\",\"T\"];function _t(t){return\"SI\"===t||\"B\"===t}function bt(t){return t>14||t<-15}function wt(t,e,r,n){var a=t<0,o=e._tickround,l=r||e.exponentformat||\"B\",c=e._tickexponent,u=Z.getTickFormat(e),h=e.separatethousands;if(n){var f={exponentformat:l,minexponent:e.minexponent,dtick:\"none\"===e.showexponent?e.dtick:i(t)&&Math.abs(t)||1,range:\"none\"===e.showexponent?e.range.map(e.r2d):[0,t||1]};yt(f),o=(Number(f._tickround)||0)+4,c=f._tickexponent,e.hoverformat&&(u=e.hoverformat)}if(u)return e._numFormat(u)(t).replace(/-/g,z);var p,d=Math.pow(10,-o)/2;if(\"none\"===l&&(c=0),(t=Math.abs(t))
\")):x=f.textLabel;var C={x:f.traceCoordinate[0],y:f.traceCoordinate[1],z:f.traceCoordinate[2],data:b._input,fullData:b,curveNumber:b.index,pointNumber:T};d.appendArrayPointValue(C,b,T),t._module.eventData&&(C=b._module.eventData(C,f,b,{},T));var L={points:[C]};if(e.fullSceneLayout.hovermode){var I=[];d.loneHover({trace:b,x:(.5+.5*v[0]/v[3])*s,y:(.5-.5*v[1]/v[3])*l,xLabel:k.xLabel,yLabel:k.yLabel,zLabel:k.zLabel,text:x,name:u.name,color:d.castHoverOption(b,T,\"bgcolor\")||u.color,borderColor:d.castHoverOption(b,T,\"bordercolor\"),fontFamily:d.castHoverOption(b,T,\"font.family\"),fontSize:d.castHoverOption(b,T,\"font.size\"),fontColor:d.castHoverOption(b,T,\"font.color\"),nameLength:d.castHoverOption(b,T,\"namelength\"),textAlign:d.castHoverOption(b,T,\"align\"),hovertemplate:h.castOption(b,T,\"hovertemplate\"),hovertemplateLabels:h.extendFlat({},C,k),eventData:[C]},{container:n,gd:r,inOut_bbox:I}),C.bbox=I[0]}f.distance<5&&(f.buttons||w)?r.emit(\"plotly_click\",L):r.emit(\"plotly_hover\",L),this.oldEventData=L}else d.loneUnhover(n),this.oldEventData&&r.emit(\"plotly_unhover\",this.oldEventData),this.oldEventData=void 0;e.drawAnnotations(e)},k.recoverContext=function(){var t=this;t.glplot.dispose();var e=function(){t.glplot.gl.isContextLost()?requestAnimationFrame(e):t.initializeGLPlot()?t.plot.apply(t,t.plotArgs):h.error(\"Catastrophic and unrecoverable WebGL error. Context lost.\")};requestAnimationFrame(e)};var M=[\"xaxis\",\"yaxis\",\"zaxis\"];function S(t,e,r){for(var n=t.fullSceneLayout,i=0;i<3;i++){var a=M[i],o=a.charAt(0),s=n[a],l=e[o],c=e[o+\"calendar\"],u=e[\"_\"+o+\"length\"];if(h.isArrayOrTypedArray(l))for(var f,p=0;p<(u||l.length);p++)if(h.isArrayOrTypedArray(l[p]))for(var d=0;d
\");y.text(_).attr(\"data-unformatted\",_).call(u.convertToTspans,t),v=c.bBox(y.node())}y.attr(\"transform\",i(-3,8-v.height)),g.insert(\"rect\",\".static-attribution\").attr({x:-v.width-6,y:-v.height-3,width:v.width+6,height:v.height+3,fill:\"rgba(255, 255, 255, 0.75)\"});var b=1;v.width+6>x&&(b=x/(v.width+6));var w=[n.l+n.w*p.x[1],n.t+n.h*(1-p.y[0])];g.attr(\"transform\",i(w[0],w[1])+a(b))}},e.updateFx=function(t){for(var e=t._fullLayout,r=e._subplots[f],n=0;n
\");_.text(T).attr(\"data-unformatted\",T).call(h.convertToTspans,t),b=u.bBox(_.node())}_.attr(\"transform\",a(-3,8-b.height)),x.insert(\"rect\",\".static-attribution\").attr({x:-b.width-6,y:-b.height-3,width:b.width+6,height:b.height+3,fill:\"rgba(255, 255, 255, 0.75)\"});var k=1;b.width+6>w&&(k=w/(b.width+6));var A=[n.l+n.w*f.x[1],n.t+n.h*(1-f.y[0])];x.attr(\"transform\",a(A[0],A[1])+o(k))}},e.updateFx=function(t){for(var e=t._fullLayout,r=e._subplots[p],n=0;n