docs: add trial data and analysis

This commit is contained in:
Julian Lobbes 2023-08-26 01:36:24 +02:00 committed by Julian Lobbes
parent 973318c6f7
commit fd62042c20
20 changed files with 3233 additions and 21 deletions

1
docs/analysis/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/.venv/

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

217
docs/analysis/charts.py Normal file
View File

@ -0,0 +1,217 @@
import matplotlib.pyplot as plt
import numpy as np
plt.rcParams['font.family'] = 'serif'
plt.rcParams['font.size'] = 12
def generate_measurement_stats_pie_chart(show: bool, save: bool):
fig, ax = plt.subplots()
size_outer = 0.3
size_inner = 0.2
vals_outer = np.array([28., 7.]) # Measurements vs Measurement failures
vals_middle = np.array([17., 11., 7., 0.]) # MEWS calculations, Measurement only, P2 failures, P1 failures
vals_inner = np.array([16., 1., 8., 3., 2., 5., 0., 0.]) # Innermost ring values
cmap = plt.get_cmap("tab20b")
outer_colors = cmap(np.array([6, 13]))
middle_colors = cmap(np.array([5, 15, 14, 14]))
inner_colors = cmap(np.array([11, 10, 11, 10, 11, 10, 11, 10]))
# Outer ring
ax.pie(vals_outer, radius=1, colors=outer_colors,
wedgeprops=dict(width=size_outer, edgecolor='w'))
# Middle ring
ax.pie(vals_middle, radius=1-size_outer, colors=middle_colors,
wedgeprops=dict(width=size_inner, edgecolor='w'))
# Innermost ring
inner_pie = ax.pie(vals_inner, radius=1-size_outer-size_inner, colors=inner_colors,
wedgeprops=dict(width=size_inner, edgecolor='w'))
#labels_inner = ["home", "on the go"]
#ax.legend(inner_pie[0], labels_inner, loc="center right")
ax.set(aspect="equal")
if save:
plt.savefig(
"chart-measurement-stats.png",
dpi=900,
bbox_inches='tight',
transparent=True
)
if show:
plt.show()
def generate_measurement_repeats_histogram(show: bool, save: bool):
device_names = ["Scanwatch", "BPM Core", "Thermo"]
labels_attempts = [f"{name} measurement attempts" for name in device_names]
labels_failures = ["S\u2081 failures", "B\u2081 failures", "T\u2081 failures"]
values_attempts = [43, 33, 28]
values_failures = [15, 5, 0]
# Define a list of colors for the bars
cmap = plt.get_cmap("tab20b")
colors_attempts = cmap(np.array([15, 11, 7]))
colors_failures = cmap(np.array([13, 9, 5]))
# Plot the bars with the specified colors and labels for the legend
bars_attempts = plt.bar(device_names, values_attempts, color=colors_attempts)
bars_failures = plt.bar(device_names, values_failures, color=colors_failures)
# Add individual labels for each bar in the legend
for i in range(3):
bars_attempts[i].set_label(labels_attempts[i])
bars_failures[i].set_label(labels_failures[i])
plt.ylabel("Count")
plt.ylim(0, 55)
plt.yticks(range(0, 56, 5))
plt.legend(loc="upper right", prop={"size":8})
if save:
plt.savefig(
"chart-measurement-repeats.png",
dpi=900,
bbox_inches="tight",
transparent=True
)
if show:
plt.show()
def generate_connection_boxplots(show: bool, save: bool):
data_downlink = [
[50.42, 46.29, 39.94, 39.44, 50.21, 44.34, 41.03, 47.88, 50.34, 41.72, 43.93, 49.34, 50.49, 52.47, 47.04, 45.86, 51.17, 50.12, 52.16, 50.55, 50.69, 53.43, 42.21, 46.81],
[15.3, 25.47, 33.91, 12.27]
]
data_uplink = [
[11.38, 11.24, 8.46, 9.33, 11.39, 9.33, 9.03, 10.65, 11.59, 8.81, 8.91, 10.56, 10.98, 10.04, 9.11, 8.62, 11.08, 10.62, 11.24, 11.29, 10.86, 10.46, 9.16, 9.15],
[5.33, 5.46, 3.38, 4.23]
]
data_rtt = [
[14, 16, 15, 15, 15, 15, 16, 12, 15, 14, 18, 14, 19, 16, 23, 12, 16, 14, 12, 13, 13, 13, 17, 15],
[145, 127, 104, 108]
]
fig, axes = plt.subplots(nrows=1, ncols=3, sharex=True, figsize=(15,5))
#plt.subplots_adjust(wspace=0.4)
cmap = plt.get_cmap("tab20b")
medianprops= {
"color": cmap(1),
"linewidth": 2,
}
# Downlink Boxplot
axes[1].boxplot(data_downlink, medianprops=medianprops)
axes[1].set_ylabel("Datarate [Mbps]")
axes[1].set_title("Downlink")
axes[1].set_xticks([1, 2])
axes[1].set_xticklabels(["At home", "On the go"])
# Uplink Boxplot
axes[0].boxplot(data_uplink, medianprops=medianprops)
axes[0].set_ylabel("Datarate [Mbps]")
axes[0].set_title("Uplink")
axes[0].set_xticks([1, 2])
axes[0].set_xticklabels(["At home", "On the go"])
# RTT Boxplot
axes[2].boxplot(data_rtt, medianprops=medianprops)
axes[2].set_ylabel("RTT [ms]")
axes[2].set_title("RTT")
axes[2].set_xticks([1, 2])
axes[2].set_xticklabels(["At home", "On the go"])
# Function to add jitter
def jitter(x, width=0.1):
"""Return the given x coordinate plus a small random offset."""
np.random.seed = 0
return x + np.random.uniform(-width, width)
# Sample points to plot on the boxplots
points_uplink = [
(1, 11.38),
(2, 5.33),
(1, 9.33),
(2, 5.46),
(1, 11.59),
(1, 10.56),
(1, 10.98),
(1, 11.24),
(2, 4.23),
(1, 10.86),
(1, 10.46),
]
points_downlink = [
(1, 50.42),
(2, 15.3),
(1, 39.44),
(2, 25.47),
(1, 50.34),
(1, 49.34),
(1, 50.49),
(1, 52.16),
(2, 12.27),
(1, 50.69),
(1, 53.43),
]
points_rtt = [
(1, 14),
(2, 145),
(1, 15),
(2, 127),
(1, 15),
(1, 14),
(1, 19),
(1, 12),
(2, 108),
(1, 13),
(1, 13),
]
# Plotting points on Uplink Boxplot
for x, y in points_uplink:
axes[0].scatter(jitter(x), y, marker='x', color=cmap(14), linewidth=0.5)
# Plotting points on Downlink Boxplot
for x, y in points_downlink:
axes[1].scatter(jitter(x), y, marker='x', color=cmap(14), linewidth=0.5)
# Plotting points on RTT Boxplot
for x, y in points_rtt:
axes[2].scatter(jitter(x), y, marker='x', color=cmap(14), linewidth=0.5)
# Plot a dummy point for the legend (outside the visible range)
#axes[0].scatter([-100], [-100], marker='x', color=cmap(14), linewidth=0.5, label='synchronization failure')
#fig.legend(loc='upper center', bbox_to_anchor=(0.5, 1.1))
if save:
plt.savefig(
"chart-connection-boxplot.png",
dpi=900,
bbox_inches="tight",
transparent=True
)
if show:
plt.show()
def main():
#generate_measurement_stats_pie_chart(False, True)
#generate_measurement_repeats_histogram(False, True)
generate_connection_boxplots(False, True)
if __name__ == "__main__":
main()

1402
docs/analysis/data.json Normal file

File diff suppressed because it is too large Load Diff

BIN
docs/analysis/data.ods Normal file

Binary file not shown.

108
docs/analysis/main.py Normal file
View File

@ -0,0 +1,108 @@
import json
from datetime import datetime, timedelta
import records as r
def get_records() -> list[r.Collection]:
"""Returns the trial data as a list of `records.Collection` objects."""
json_data = []
with open('data.json', 'r') as file:
json_data = json.load(file)
def incremented(time: datetime) -> datetime:
if time.hour == 22:
time += timedelta(hours=12)
else:
time += timedelta(hours=3)
return time
records: list[r.Collection] = []
time = datetime.fromisoformat("2023-08-14T10:00:00")
for item in json_data:
p1 = item['p1']
home_environment = item['home']
if p1:
mews = None
blood_pressure = None
body_temp = None
heart_rate = None
respiration_score = None
spo2 = None
s1 = None
b1 = None
t1 = None
p2 = None
s2 = None
b2 = None
t2 = None
uplink = None
downlink = None
rtt = None
else:
blood_pressure = r.BloodPressure(
time=datetime.fromisoformat(item['blood_pressure']['time']),
value_systolic=item['blood_pressure']['value_systolic'],
value_diastolic=item['blood_pressure']['value_systolic']
)
body_temp = r.BodyTemp(
time=datetime.fromisoformat(item['body_temp']['time']),
value=item['body_temp']['value']
)
heart_rate = r.HeartRate(
time=datetime.fromisoformat(item['heart_rate']['time']),
value=item['heart_rate']['value']
)
respiration_score = r.RespirationScore(
time=datetime.fromisoformat(item['respiration_score']['time']),
value=item['respiration_score']['value']
)
spo2 = r.Spo2(
time=datetime.fromisoformat(item['spo2']['time']),
value=item['spo2']['value']
)
s1 = item['s1']
b1 = item['b1']
t1 = item['t1']
p2 = item['p2']
s2 = item['s2']
b2 = item['b2']
t2 = item['t2']
uplink = item['uplink']
downlink = item['downlink']
rtt = item['rtt']
if s2 or b2 or t2:
mews = None
else:
mews = r.Mews(
time=datetime.fromisoformat(item['mews']['time']),
value=item['mews']['value']
)
records.append(r.Collection(
notification_time=time,
mews=mews,
blood_pressure=blood_pressure,
body_temp=body_temp,
heart_rate=heart_rate,
respiration_score=respiration_score,
spo2=spo2,
s1=s1,
b1=b1,
t1=t1,
s2=s2,
b2=b2,
t2=t2,
p1=p1,
p2=p2,
home_environment=home_environment,
uplink=uplink,
downlink=downlink,
rtt=rtt
))
time = incremented(time)
return records

65
docs/analysis/records.py Normal file
View File

@ -0,0 +1,65 @@
"""Type classes used during analysis."""
from datetime import datetime
from dataclasses import dataclass
from typing import Optional
@dataclass
class BloodPressure:
time: datetime
value_diastolic: int
value_systolic: int
@dataclass
class BodyTemp:
time: datetime
value: float
@dataclass
class HeartRate:
time: datetime
value: int
@dataclass
class Mews:
time: datetime
value: int
@dataclass
class RespirationScore:
time: datetime
value: int
@dataclass
class Spo2:
time: datetime
value: int
@dataclass
class Collection:
notification_time: datetime
mews: Optional[Mews]
blood_pressure: Optional[BloodPressure]
body_temp: Optional[BodyTemp]
heart_rate: Optional[HeartRate]
respiration_score: Optional[RespirationScore]
spo2: Optional[Spo2]
s1: Optional[int]
b1: Optional[int]
t1: Optional[int]
s2: Optional[bool]
b2: Optional[bool]
t2: Optional[bool]
p1: bool
p2: Optional[bool]
home_environment: bool
uplink: Optional[float]
downlink: Optional[float]
rtt: Optional[int]

14
docs/analysis/shell.nix Normal file
View File

@ -0,0 +1,14 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
nativeBuildInputs = with pkgs; [
python3Packages.numpy
python3Packages.matplotlib
python310Packages.pyqt6
python310Packages.pyside6
qt6.qtwayland
];
shellHook = ''
export MPLBACKEND=QtAgg
'';
}

View File

@ -1187,3 +1187,52 @@ Very low and high {EWS} are able to discriminate between patients who are not li
pmid = {30247849}, pmid = {30247849},
file = {Printable HTML:/home/ulinja/Zotero/storage/ISD8WIRR/NBK525974.html:text/html}, file = {Printable HTML:/home/ulinja/Zotero/storage/ISD8WIRR/NBK525974.html:text/html},
} }
@online{noauthor_worlds_nodate,
title = {The worlds first analog watch with clinically validated {ECG} - {ScanWatch} {\textbar} Withings},
url = {https://www.withings.com/de/en/scanwatch},
abstract = {The hybrid smartwatch that detects atrial fibrillation, with {ECG} and an oximeter for {SpO}2 measurements—plus activity and sleep tracking.},
urldate = {2023-08-22},
langid = {english},
file = {Snapshot:/home/ulinja/Zotero/storage/7ZR5X6SG/scanwatch.html:text/html},
}
@online{noauthor_scanwatch_nodate,
title = {{ScanWatch} - Performing a {SpO}2 measurement},
url = {https://support.withings.com/hc/en-us/articles/360010097498-ScanWatch-Performing-a-SpO2-measurement},
abstract = {Disclaimer: Due to health regulations and clearances, some {ScanWatch} features may not be available or may not have clinical validation in your region. Learn more.
The result of a one-time measureme...},
titleaddon = {Withings {\textbar} Support},
urldate = {2023-08-22},
langid = {american},
file = {Snapshot:/home/ulinja/Zotero/storage/SXLEIBSV/360010097498-ScanWatch-Performing-a-SpO2-measurement.html:text/html},
}
@online{noauthor_bpm_nodate,
title = {{BPM} Core {\textbar} Withings},
url = {https://www.withings.com/de/en/bpm-core},
abstract = {The first Wi-Fi blood pressure monitor with {ECG} and a digital stethoscope to detect cardiovascular conditions like atrial fibrillation and valvular heart disease.},
urldate = {2023-08-22},
langid = {english},
file = {Snapshot:/home/ulinja/Zotero/storage/UTB8VF5C/bpm-core.html:text/html},
}
@online{noauthor_bpm_nodate-1,
title = {{BPM} Core - Positioning myself and the {BPM} for the measurement},
url = {https://support.withings.com/hc/en-us/articles/360024168954-BPM-Core-Positioning-myself-and-the-BPM-for-the-measurement},
abstract = {Note: With Firmware Release 841, we have introduced an optional Heart Sound Measurement Assistance Mode which can provide real-time feedback for correct positioning. Read about it here. 
 
{BPM} Cor...},
titleaddon = {Withings {\textbar} Support},
urldate = {2023-08-22},
langid = {american},
file = {Snapshot:/home/ulinja/Zotero/storage/8LN5S3XU/360024168954-BPM-Core-Positioning-myself-and-the-BPM-for-the-measurement.html:text/html},
}
@online{noauthor_guides_nodate,
title = {Guides documents {\textbar} Withings},
url = {https://www.withings.com/de/en/guides},
urldate = {2023-08-22},
file = {Guides documents | Withings:/home/ulinja/Zotero/storage/IJU54NKT/guides.html:text/html},
}

947
docs/misc/trial-data.json Normal file
View File

@ -0,0 +1,947 @@
[
{
"blood_pressure": {
"time": "2023-08-14T10:26:35",
"value_diastolic": 86,
"value_systolic": 131
},
"body_temp": {
"time": "2023-08-14T10:25:48",
"value": 36.98
},
"heart_rate": {
"time": "2023-08-14T10:26:35",
"value": 90
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": "2023-08-14T10:24:51",
"value": 0
},
"spo2": {
"time": "2023-08-14T10:25:35",
"value": 96
}
},
{
"blood_pressure": {
"time": "2023-08-14T13:05:06",
"value_diastolic": 75,
"value_systolic": 117
},
"body_temp": {
"time": "2023-08-14T13:04:18",
"value": 37.49
},
"heart_rate": {
"time": "2023-08-14T13:05:06",
"value": 71
},
"mews": {
"time": "2023-08-14T13:06:18",
"value": 0
},
"respiration_score": {
"time": "2023-08-14T13:03:24",
"value": 0
},
"spo2": {
"time": "2023-08-14T13:03:59",
"value": 97
}
},
{
"blood_pressure": {
"time": "2023-08-14T16:48:16",
"value_diastolic": 85,
"value_systolic": 126
},
"body_temp": {
"time": "2023-08-14T16:47:12",
"value": 37.16
},
"heart_rate": {
"time": "2023-08-14T16:48:16",
"value": 73
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": "2023-08-14T16:46:18",
"value": 0
},
"spo2": {
"time": "2023-08-14T16:46:57",
"value": 98
}
},
{
"blood_pressure": {
"time": "2023-08-14T19:06:01",
"value_diastolic": 86,
"value_systolic": 124
},
"body_temp": {
"time": "2023-08-14T19:05:10",
"value": 37.28
},
"heart_rate": {
"time": "2023-08-14T19:06:01",
"value": 82
},
"mews": {
"time": "2023-08-14T19:07:22",
"value": 0
},
"respiration_score": {
"time": "2023-08-14T19:03:39",
"value": 0
},
"spo2": {
"time": "2023-08-14T19:04:57",
"value": 96
}
},
{
"blood_pressure": {
"time": "2023-08-14T22:27:50",
"value_diastolic": 86,
"value_systolic": 114
},
"body_temp": {
"time": "2023-08-14T22:27:12",
"value": 37.32
},
"heart_rate": {
"time": "2023-08-14T22:27:50",
"value": 56
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": "2023-08-14T22:24:14",
"value": 0
},
"spo2": {
"time": "2023-08-14T22:26:54",
"value": 98
}
},
{
"blood_pressure": {
"time": null,
"value_diastolic": null,
"value_systolic": null
},
"body_temp": {
"time": null,
"value": null
},
"heart_rate": {
"time": null,
"value": null
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": null,
"value": null
},
"spo2": {
"time": null,
"value": null
}
},
{
"blood_pressure": {
"time": null,
"value_diastolic": null,
"value_systolic": null
},
"body_temp": {
"time": null,
"value": null
},
"heart_rate": {
"time": null,
"value": null
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": null,
"value": null
},
"spo2": {
"time": null,
"value": null
}
},
{
"blood_pressure": {
"time": "2023-08-15T16:13:28",
"value_diastolic": 83,
"value_systolic": 115
},
"body_temp": {
"time": "2023-08-15T16:12:25",
"value": 37.64
},
"heart_rate": {
"time": "2023-08-15T16:13:28",
"value": 84
},
"mews": {
"time": "2023-08-15T16:16:55",
"value": 0
},
"respiration_score": {
"time": "2023-08-15T16:11:23",
"value": 0
},
"spo2": {
"time": "2023-08-15T16:12:03",
"value": 96
}
},
{
"blood_pressure": {
"time": "2023-08-15T19:37:37",
"value_diastolic": 80,
"value_systolic": 114
},
"body_temp": {
"time": "2023-08-15T19:36:32",
"value": 37.33
},
"heart_rate": {
"time": "2023-08-15T19:37:37",
"value": 75
},
"mews": {
"time": "2023-08-15T19:40:31",
"value": 0
},
"respiration_score": {
"time": "2023-08-15T19:35:07",
"value": 0
},
"spo2": {
"time": "2023-08-15T19:36:19",
"value": 97
}
},
{
"blood_pressure": {
"time": "2023-08-15T22:37:09",
"value_diastolic": 78,
"value_systolic": 114
},
"body_temp": {
"time": "2023-08-15T22:36:27",
"value": 37.27
},
"heart_rate": {
"time": "2023-08-15T22:37:09",
"value": 63
},
"mews": {
"time": "2023-08-15T22:37:55",
"value": 0
},
"respiration_score": {
"time": "2023-08-15T22:35:34",
"value": 0
},
"spo2": {
"time": "2023-08-15T22:36:09",
"value": 99
}
},
{
"blood_pressure": {
"time": "2023-08-16T10:33:29",
"value_diastolic": 82,
"value_systolic": 112
},
"body_temp": {
"time": "2023-08-16T10:32:39",
"value": 37.39
},
"heart_rate": {
"time": "2023-08-16T10:33:29",
"value": 93
},
"mews": {
"time": "2023-08-16T10:34:59",
"value": 0
},
"respiration_score": {
"time": "2023-08-16T10:31:40",
"value": 0
},
"spo2": {
"time": "2023-08-16T10:32:23",
"value": 96
}
},
{
"blood_pressure": {
"time": "2023-08-16T14:38:04",
"value_diastolic": 93,
"value_systolic": 119
},
"body_temp": {
"time": "2023-08-16T14:36:55",
"value": 37.65
},
"heart_rate": {
"time": "2023-08-16T14:38:04",
"value": 72
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": "2023-08-16T14:36:04",
"value": 0
},
"spo2": {
"time": "2023-08-16T14:36:42",
"value": 97
}
},
{
"blood_pressure": {
"time": "2023-08-16T16:47:26",
"value_diastolic": 83,
"value_systolic": 118
},
"body_temp": {
"time": "2023-08-16T16:45:42",
"value": 37.64
},
"heart_rate": {
"time": "2023-08-16T16:47:26",
"value": 89
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": "2023-08-16T16:44:39",
"value": 0
},
"spo2": {
"time": "2023-08-16T16:45:28",
"value": 98
}
},
{
"blood_pressure": {
"time": "2023-08-16T20:12:49",
"value_diastolic": 81,
"value_systolic": 118
},
"body_temp": {
"time": "2023-08-16T20:11:58",
"value": 37.52
},
"heart_rate": {
"time": "2023-08-16T20:12:49",
"value": 97
},
"mews": {
"time": "2023-08-16T20:15:42",
"value": 0
},
"respiration_score": {
"time": "2023-08-16T20:10:52",
"value": 0
},
"spo2": {
"time": "2023-08-16T20:11:35",
"value": 98
}
},
{
"blood_pressure": {
"time": "2023-08-16T23:18:08",
"value_diastolic": 74,
"value_systolic": 125
},
"body_temp": {
"time": "2023-08-16T23:15:39",
"value": 37.65
},
"heart_rate": {
"time": "2023-08-16T23:18:08",
"value": 85
},
"mews": {
"time": "2023-08-16T23:18:50",
"value": 0
},
"respiration_score": {
"time": "2023-08-16T23:13:39",
"value": 0
},
"spo2": {
"time": "2023-08-16T23:15:17",
"value": 99
}
},
{
"blood_pressure": {
"time": "2023-08-17T11:01:05",
"value_diastolic": 79,
"value_systolic": 112
},
"body_temp": {
"time": "2023-08-17T11:00:04",
"value": 37.05
},
"heart_rate": {
"time": "2023-08-17T11:01:05",
"value": 87
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": "2023-08-17T10:58:59",
"value": 0
},
"spo2": {
"time": "2023-08-17T10:59:48",
"value": 95
}
},
{
"blood_pressure": {
"time": "2023-08-17T14:27:40",
"value_diastolic": 82,
"value_systolic": 118
},
"body_temp": {
"time": "2023-08-17T14:27:09",
"value": 37.43
},
"heart_rate": {
"time": "2023-08-17T14:27:40",
"value": 80
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": "2023-08-17T14:25:59",
"value": 0
},
"spo2": {
"time": "2023-08-17T14:26:45",
"value": 98
}
},
{
"blood_pressure": {
"time": "2023-08-17T16:53:03",
"value_diastolic": 71,
"value_systolic": 121
},
"body_temp": {
"time": "2023-08-17T16:52:21",
"value": 37.73
},
"heart_rate": {
"time": "2023-08-17T16:53:03",
"value": 83
},
"mews": {
"time": "2023-08-17T16:54:15",
"value": 0
},
"respiration_score": {
"time": "2023-08-17T16:50:34",
"value": 0
},
"spo2": {
"time": "2023-08-17T16:51:58",
"value": 98
}
},
{
"blood_pressure": {
"time": "2023-08-17T20:12:24",
"value_diastolic": 69,
"value_systolic": 129
},
"body_temp": {
"time": "2023-08-17T20:11:32",
"value": 37.46
},
"heart_rate": {
"time": "2023-08-17T20:12:24",
"value": 73
},
"mews": {
"time": "2023-08-17T20:13:09",
"value": 0
},
"respiration_score": {
"time": "2023-08-17T20:10:31",
"value": 0
},
"spo2": {
"time": "2023-08-17T20:11:13",
"value": 96
}
},
{
"blood_pressure": {
"time": "2023-08-17T22:47:55",
"value_diastolic": 81,
"value_systolic": 121
},
"body_temp": {
"time": "2023-08-17T22:47:18",
"value": 36.79
},
"heart_rate": {
"time": "2023-08-17T22:47:55",
"value": 95
},
"mews": {
"time": "2023-08-17T22:50:02",
"value": 0
},
"respiration_score": {
"time": "2023-08-17T22:46:04",
"value": 0
},
"spo2": {
"time": "2023-08-17T22:46:54",
"value": 97
}
},
{
"blood_pressure": {
"time": "2023-08-18T10:54:12",
"value_diastolic": 75,
"value_systolic": 124
},
"body_temp": {
"time": "2023-08-18T10:53:27",
"value": 37.34
},
"heart_rate": {
"time": "2023-08-18T10:54:12",
"value": 73
},
"mews": {
"time": "2023-08-18T10:55:50",
"value": 0
},
"respiration_score": {
"time": "2023-08-18T10:51:36",
"value": 0
},
"spo2": {
"time": "2023-08-18T10:53:12",
"value": 96
}
},
{
"blood_pressure": {
"time": "2023-08-18T13:32:51",
"value_diastolic": 86,
"value_systolic": 123
},
"body_temp": {
"time": "2023-08-18T13:32:16",
"value": 37.09
},
"heart_rate": {
"time": "2023-08-18T13:32:51",
"value": 61
},
"mews": {
"time": "2023-08-18T13:34:01",
"value": 0
},
"respiration_score": {
"time": "2023-08-18T13:29:25",
"value": 0
},
"spo2": {
"time": "2023-08-18T13:31:55",
"value": 97
}
},
{
"blood_pressure": {
"time": "2023-08-18T16:39:14",
"value_diastolic": 83,
"value_systolic": 122
},
"body_temp": {
"time": "2023-08-18T16:37:22",
"value": 37.13
},
"heart_rate": {
"time": "2023-08-18T16:39:14",
"value": 73
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": "2023-08-18T16:36:14",
"value": 0
},
"spo2": {
"time": "2023-08-18T16:37:02",
"value": 96
}
},
{
"blood_pressure": {
"time": "2023-08-18T19:38:58",
"value_diastolic": 75,
"value_systolic": 123
},
"body_temp": {
"time": "2023-08-18T19:36:37",
"value": 37.65
},
"heart_rate": {
"time": "2023-08-18T19:38:58",
"value": 114
},
"mews": {
"time": "2023-08-18T19:41:43",
"value": 2
},
"respiration_score": {
"time": "2023-08-18T19:35:24",
"value": 0
},
"spo2": {
"time": "2023-08-18T19:36:14",
"value": 95
}
},
{
"blood_pressure": {
"time": null,
"value_diastolic": null,
"value_systolic": null
},
"body_temp": {
"time": null,
"value": null
},
"heart_rate": {
"time": null,
"value": null
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": null,
"value": null
},
"spo2": {
"time": null,
"value": null
}
},
{
"blood_pressure": {
"time": "2023-08-19T11:36:26",
"value_diastolic": 76,
"value_systolic": 121
},
"body_temp": {
"time": "2023-08-19T11:35:24",
"value": 37.5
},
"heart_rate": {
"time": "2023-08-19T11:36:26",
"value": 54
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": "2023-08-19T11:34:30",
"value": 0
},
"spo2": {
"time": "2023-08-19T11:35:05",
"value": 98
}
},
{
"blood_pressure": {
"time": "2023-08-19T13:24:56",
"value_diastolic": 73,
"value_systolic": 120
},
"body_temp": {
"time": "2023-08-19T13:24:02",
"value": 37.31
},
"heart_rate": {
"time": "2023-08-19T13:24:56",
"value": 106
},
"mews": {
"time": "2023-08-19T13:27:13",
"value": 1
},
"respiration_score": {
"time": "2023-08-19T13:21:46",
"value": 0
},
"spo2": {
"time": "2023-08-19T13:23:43",
"value": 96
}
},
{
"blood_pressure": {
"time": null,
"value_diastolic": null,
"value_systolic": null
},
"body_temp": {
"time": null,
"value": null
},
"heart_rate": {
"time": null,
"value": null
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": null,
"value": null
},
"spo2": {
"time": null,
"value": null
}
},
{
"blood_pressure": {
"time": null,
"value_diastolic": null,
"value_systolic": null
},
"body_temp": {
"time": null,
"value": null
},
"heart_rate": {
"time": null,
"value": null
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": null,
"value": null
},
"spo2": {
"time": null,
"value": null
}
},
{
"blood_pressure": {
"time": null,
"value_diastolic": null,
"value_systolic": null
},
"body_temp": {
"time": null,
"value": null
},
"heart_rate": {
"time": null,
"value": null
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": null,
"value": null
},
"spo2": {
"time": null,
"value": null
}
},
{
"blood_pressure": {
"time": null,
"value_diastolic": null,
"value_systolic": null
},
"body_temp": {
"time": null,
"value": null
},
"heart_rate": {
"time": null,
"value": null
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": null,
"value": null
},
"spo2": {
"time": null,
"value": null
}
},
{
"blood_pressure": {
"time": "2023-08-20T13:21:35",
"value_diastolic": 82,
"value_systolic": 119
},
"body_temp": {
"time": "2023-08-20T13:20:44",
"value": 37.29
},
"heart_rate": {
"time": "2023-08-20T13:21:35",
"value": 83
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": "2023-08-20T13:18:46",
"value": 0
},
"spo2": {
"time": "2023-08-20T13:20:20",
"value": 98
}
},
{
"blood_pressure": {
"time": "2023-08-20T16:13:04",
"value_diastolic": 79,
"value_systolic": 118
},
"body_temp": {
"time": "2023-08-20T16:12:05",
"value": 37.38
},
"heart_rate": {
"time": "2023-08-20T16:13:04",
"value": 95
},
"mews": {
"time": null,
"value": null
},
"respiration_score": {
"time": "2023-08-20T16:10:40",
"value": 0
},
"spo2": {
"time": "2023-08-20T16:11:52",
"value": 97
}
},
{
"blood_pressure": {
"time": "2023-08-20T19:06:47",
"value_diastolic": 82,
"value_systolic": 120
},
"body_temp": {
"time": "2023-08-20T19:05:37",
"value": 37.25
},
"heart_rate": {
"time": "2023-08-20T19:06:47",
"value": 93
},
"mews": {
"time": "2023-08-20T19:12:52",
"value": 0
},
"respiration_score": {
"time": "2023-08-20T19:04:47",
"value": 0
},
"spo2": {
"time": "2023-08-20T19:05:24",
"value": 97
}
},
{
"blood_pressure": {
"time": "2023-08-20T22:25:47",
"value_diastolic": 73,
"value_systolic": 115
},
"body_temp": {
"time": "2023-08-20T22:25:06",
"value": 37.45
},
"heart_rate": {
"time": "2023-08-20T22:25:47",
"value": 83
},
"mews": {
"time": "2023-08-20T22:29:09",
"value": 0
},
"respiration_score": {
"time": "2023-08-20T22:23:27",
"value": 0
},
"spo2": {
"time": "2023-08-20T22:24:51",
"value": 97
}
}
]

BIN
docs/misc/trial-data.ods Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

View File

@ -34,3 +34,30 @@
The blood oxygen saturation represents the proportion of hemoglobin molecules in the bloodstream that are saturated with oxygen\cite{hafen_oxygen_2023}. The blood oxygen saturation represents the proportion of hemoglobin molecules in the bloodstream that are saturated with oxygen\cite{hafen_oxygen_2023}.
} }
} }
\newglossaryentry{uplink-datarate}{
name={uplink datarate},
description={
The speed at which data is transmitted from a client device, such as a computer or smartphone, to a server or central network.
Typically measured in Mbps (megabits per second), it represents the efficiency of data sending capabilities of a network connection.
}
}
\newglossaryentry{downlink-datarate}{
name={downlink datarate},
description={
The rate at which data is received by a client device from a central server or network.
Expressed often in Mbps, it reflects the downloading or data reception efficiency of a network connection.
}
}
\newglossaryentry{rtt}{
type=\acronymtype,
name={RTT},
description={\Gls{rtt_full}},
first={\Gls{rtt_full} (RTT)}
}
\newglossaryentry{rtt_full}{
name={Round trip time},
description={
The time taken for a data packet to travel from a source to a destination and back again.
It provides an indication of the latency or delay inherent in a network connection and is usually measured in milliseconds (ms).
}
}

View File

@ -14,11 +14,15 @@
\usepackage{titlesec} \usepackage{titlesec}
\usepackage{fancyhdr} \usepackage{fancyhdr}
\usepackage{color} \usepackage{booktabs} % thick horizontal lines
\usepackage{multirow}
\usepackage{array,tabularx}
\usepackage[bf]{caption} % custom table captions
\usepackage[table]{xcolor}
\usepackage[colorlinks]{hyperref} \usepackage[colorlinks]{hyperref}
\usepackage{tcolorbox} \usepackage[most]{tcolorbox}
\tcbuselibrary{most}
\pagestyle{plain} \pagestyle{plain}
\usepackage{pdfpages} % PDFs in appendix
\usepackage[toc,nopostdot,nonumberlist,style=altlist,acronym]{glossaries} \usepackage[toc,nopostdot,nonumberlist,style=altlist,acronym]{glossaries}
% Page margins % Page margins
@ -84,17 +88,16 @@
% A command which generates a TODO message % A command which generates a TODO message
\newcommand{\todo}[1]{{\fontfamily{lmtt}\selectfont{\color{orange}\small\underline{TODO:}} \textbf{#1}\normalsize \\}} \newcommand{\todo}[1]{{\fontfamily{lmtt}\selectfont{\color{orange}\small\underline{TODO:}} \textbf{#1}\normalsize \\}}
\input{./glossary.tex} \input{./glossary.tex}
\renewcommand{\thepage}{\Roman{page}}
\begin{document} \begin{document}
%{\fontfamily{phv}\selectfont}
\pagenumbering{Roman}
\input{cover.tex} \input{cover.tex}
\pagenumbering{Roman}
\section*{Summary} \section*{Summary}
\addcontentsline{toc}{section}{Summary} \addcontentsline{toc}{section}{Summary}
@ -253,7 +256,7 @@ process in Section~\ref{sec:modules}.
The centralized server components, including the Gotify server, a task scheduler used to schedule sending notifications and the Medwings application code itself are deployed The centralized server components, including the Gotify server, a task scheduler used to schedule sending notifications and the Medwings application code itself are deployed
on a publicly accessible web server using a Docker container environment. on a publicly accessible web server using a Docker container environment.
\subsection{Design Challenges} \subsection{Design Challenges}\label{sec:design-challenges}
Since managing a user in Medwings requires the respective user's state to be mirrored by two other services, Withings and Gotify, keeping user accounts across Since managing a user in Medwings requires the respective user's state to be mirrored by two other services, Withings and Gotify, keeping user accounts across
all three services in sync presents a challenge. all three services in sync presents a challenge.
@ -286,27 +289,152 @@ error message, prompting them to repeat the measurements.
\newpage \newpage
\section{System Interaction and Usability} \section{Methodology}
\begin{itemize}
\item Personal experiences in interacting with the system and the medical devices
\item What went well and what didn't? Why?
\item Usability for potential medical staff, if applicable
\item Strengths and weaknesses of patients taking their own measurements vs. having a medical professional take them
\item How well did patients (or you, in this case) adhere to the measurement schedule? What factors influenced this?
\end{itemize}
Following the development and deployment of the application, Medwings underwent a performance and usability study.
A test subject, impersonating a patient, used the application daily over the course of one week.
Each day, five notifications were dispatched every three hours, starting at 10 AM.
When prompted by a notification, the subject was asked to take vital sign readings using the three Withings medical sensors,
followed by a MEWS calculation carried out by Medwings.
The values of the vitals and MEWS records, as well as the time of measurement, were recorded by the application.
For each MEWS calculation, the test subject manually kept track of which type of environment the system was used in.
A distinction was made between the following environments:
\begin{itemize}
\item At home: The subject was located at home, and their phone had access to a low latency broadband internet connection
\item On the go: The subject was away from home, and their phone had access to a high latency mobile internet connection
\end{itemize}
Preceeding each MEWS measurement, the maximum data transmission rates, both uplink and downlink, as well as the average connection round trip time
from the subject's phone to a distant reference server was measured.
The location of the reference server was kept constant throughout the trial.
The occurrence of some measurement failures was anticipated, and manually recorded.
Measurement failures were categorized into eight distinct classes, as listed in Table~\ref{tab:measurement-failures}.
\begin{table}[!ht]
\centering
\begin{tcolorbox}[
enhanced, width=0.95\linewidth, boxrule=2pt, arc=4pt,
tabularx={>{\centering\arraybackslash}c|>{\centering\arraybackslash}c|>{\arraybackslash}X}
]
\textbf{Device} & \textbf{Failure Class} & \textbf{Description} \\
\specialrule{2pt}{0em}{0em}
\multirow{2}{*}{Scanwatch} & $S_1$ & Device aborted measurement \\
\cline{2-3}
& $S_2$ & Measurement synchronization failure \\
\hline
\multirow{2}{*}{BPM Core} & $B_1$ & Device aborted measurement \\
\cline{2-3}
& $B_2$ & Measurement synchronization failure \\
\hline
\multirow{2}{*}{Thermo} & $T_1$ & Device aborted measurement \\
\cline{2-3}
& $T_2$ & Measurement synchronization failure \\
\hline
\multirow{2}{*}{---} & $P_1$ & Patient did not take any measurements \\
\cline{2-3}
& $P_2$ & MEWS calculation timed out \\
\end{tcolorbox}
\caption{\label{tab:measurement-failures}Classification of measurement failures during the usability trial}
\end{table}
The Scanwatch and BPM Core are equipped with accellerometers\cite{noauthor_worlds_nodate, noauthor_bpm_nodate}.
If erratic movement is detected, the devices abort the measurement to avoid misinterpretation of sensor readings.
Similarly, upon failure to process captured sensor data into a plausible result, a measurement may be aborted by the device\cite{noauthor_scanwatch_nodate, noauthor_bpm_nodate-1, noauthor_guides_nodate}.
The measurement failure classes $S_1$, $B_1$ and $T_1$ were used to record these kinds of failure for each respective device.
Following an $S_1$, $B_1$ or $T_1$ failure, the subject repeatedly carried out measurements unsing the affected device until a valid reading could be obtained.
Subsequent failures were also recorded.
As explained in Section~\ref{sec:design-challenges}, following a successful reading, a device may fail to push the measurement data to the Withings
Cloud within the ten minute validity range for a MEWS calculation imposed by Medwings.
Depending on which device failed to synchronize its data within the allowed time, a $S_2$, $B_2$ or $T_2$ failure was recorded.
If the subject did not carry out any vitals measurements despite being prompted by a notification, a $P_1$ failure was noted.
Finally, if the patient failed to carry out all three required measurements within the ten minute time limit, a $P_2$ failure was recorded.
Following $S_2$, $B_2$, $T_2$, $P_1$ and $P_2$ failures, the measurement process was not repeated until the next notification.
\newpage \newpage
\section{Data Presentation and Analysis} \section{Data Presentation and Analysis}
\begin{itemize}
\item Present your measured vitals data in an organized and visual manner (tables, graphs, etc.) The trial period encompassed seven days, on each of which five notifications were dispatched to the patient.
\item Analyze the vitals and MEWS data: trends, anomalies, etc. Thus, an overall of $N=35$ system interactions were recorded.
\item Discuss the regularity of measurements and the systems effectiveness in prompting and collecting data The patient was at home during $26$ of these, and on the go in all other cases.
\end{itemize}
Seven $P_1$ measurement failures occurred, wherein the patient did not react to a received notification by taking measurements.
Out of these, five occurred while the patient was on the go and, notably, four $P_1$ failures were consequtive, stemming from a period of 24 hours
during which the patient did not have access to the smart devices.
No $P_2$ measurement failures occured during the trial.
In total, vitals were measured using all three devices in $28$ cases.
However, in $11$ cases, at least one device failed to synchronize its readings within the ten minute timeout.
Throughout the trial, $17$ MEWS calculations were recorded successfully.
Figure~\ref{fig:measurement-stats} visualizes the overall measurement and failure counts.
\begin{figure}[!ht]
\begin{center}
\includegraphics[width=\textwidth]{./figures/chart-measurement-stats.png}
\caption{\label{fig:measurement-stats}Measurement and measurement failure statistics at home and on the go.}
\end{center}
\end{figure}
Out of $84$ successful individual measurements across all devices throughout the trial, $18\%$ took longer than premitted by Medwings to synchronize with the Withings Cloud.
Particularly while on the go, synchronization was prone to taking too long: $25\%$ of measurements resulted in synchronization failure, compared
to $11\%$ at home.
Especially the BPM Core and Thermo devices suffered from slow synchronization times: in a total of $15$ synchronization timeouts, $n_{B_2} = 7$ were caused
by the blood pressure meter, and $n_{T_2} = 7$ by the thermometer.
Three causing factors were identified: firstly, while the Scanwatch is constantly connected to the patient's phone, the BPM Core and Thermo devices only establish
a connection intermittently.
Presumably, measurement data updates from the smart device to the phone are sent less frequently.
The second factor becomes apparent when examining the likelihood of each device aborting a measurement due to inconclusive sensor data,
as displayed in Figure~\ref{fig:measurement-repeats}:
for the BPM Core, $15\%$ of attempted measurements had to be repeated ($n_{B_1} = 5$).
For the Scanwatch, over $34\%$ of readings ($n_{S_1} = 15$) were inconclusive and had to be repeated.
The Withings Thermo did not abort any measurements ($n_{T_1} = 0$).
Although aborted measurements did not cause synchronization failures directly, the time taken to repeat measurements did impact
the likelihood of the MEWS calculation timing out before all vitals data was synchronized.
\begin{figure}[!ht]
\begin{center}
\includegraphics[width=0.8\textwidth]{./figures/chart-measurement-repeats.png}
\caption{\label{fig:measurement-repeats}Number of measurement attempts and aborted measurements for each smart device.}
\end{center}
\end{figure}
Figure~\ref{fig:connection-boxplot} illustrates the comparative boxplots for the \gls{downlink-datarate}, \gls{uplink-datarate}, and \Gls{rtt} connection metrics when
the patient was at home versus on the go.
While there are evident differences in the distributions of these metrics between the two environments, the points representing synchronization failures do not
predominantly cluster around areas of low datarate or high \Gls{rtt}.
\begin{figure}[!ht]
\begin{center}
\includegraphics[width=\textwidth]{./figures/chart-connection-boxplot.png}
\caption{\label{fig:connection-boxplot}Connection quality and synchronization failures.}
\end{center}
\end{figure}
A reaction delay $t_r$ existed from when a notification was dispatched until the subject visited the Medwings website to take measurements.
The average ($\overline{t_{r,\text{home}}} = 36\text{ min}$) and median ($M_{t_{r,\text{home}}} = 33\text{ min}$) delay was significantly lower
while the patient was at home, compared to when they were out of the house ($\overline{t_{r,\text{on the go}}} = 68\text{ min}$, $M_{t_{r,\text{on the go}}} = 70\text{ min}$).
The average time taken to carry out all three measurements was $4.5$ minutes in both enviroments.
In all cases where vitals measurements were taken using the devices, the vitals data was captured and stored by Medwings.
All MEWS calculations carried out by the application returned the correct value as expected from the vitals data they were based upon.
The subject's vital signs were within their normal ranges representative of the patient's age for all measurements, with the exception of
two outliers where a slightly increased heart rate was measured.
Both outliers were detected correctly by Medwings.
The complete trial data can be seen in Appendix~\ref{apdx:trial-data}.
\newpage \newpage
\section{Evaluation and Validation} \section{Evaluation and Validation}
% - Personal experiences in interacting with the system and the medical devices
% - What went well and what didn't? Why?
% - Usability for potential medical staff, if applicable
% - Strengths and weaknesses of patients taking their own measurements vs. having a medical professional take them
% - How well did patients (or you, in this case) adhere to the measurement schedule? What factors influenced this?
\begin{itemize} \begin{itemize}
\item Describe the methods used to evaluate the system \item Describe the methods used to evaluate the system
\item Discuss the validity of the study, especially considering that the test patient was yourself \item Discuss the validity of the study, especially considering that the test patient was yourself
@ -314,6 +442,9 @@ error message, prompting them to repeat the measurements.
\item Are the results likely to generalize? Why or why not? \item Are the results likely to generalize? Why or why not?
\end{itemize} \end{itemize}
% The absence of a clear correlation suggests that synchronization failures were not majorly influenced by the fundamental connection quality metrics.
% The root cause of these failures likely lies elsewhere and is not solely dictated by raw connection performance.
\newpage \newpage
\section{Lessons Learned and Reflections} \section{Lessons Learned and Reflections}
@ -352,14 +483,20 @@ error message, prompting them to repeat the measurements.
\newpage \newpage
\listoffigures \listoffigures
\addcontentsline{toc}{section}{List of figures}
\newpage \newpage
\listoftables \listoftables
\addcontentsline{toc}{section}{List of tables}
\newpage \newpage
\printbibliography \printbibliography
\addcontentsline{toc}{section}{References}
% Appendix here \newpage
\appendix
\includepdf[pages=1, scale=0.95, pagecommand={\vspace*{-2.75cm}\section{Trial Data}\label{apdx:trial-data}}, angle=-90]{./attachments/trial-data.pdf}
\includepdf[pages=2-, scale=0.95, pagecommand={}, angle=-90]{./attachments/trial-data.pdf}
\newpage \newpage
\section*{Ehrenwörtliche Erklärung} \section*{Ehrenwörtliche Erklärung}