Personal Health Records, published by HL7 International / Patient Empowerment. This guide is not an authorized publication; it is the continuous build for version 1.0.0-ballot2 built by the FHIR (HL7® FHIR® Standard) CI Build. This version is based on the current content of https://github.com/HL7/personal-health-record-format-ig/ and changes regularly. See the Directory of published versions
| Page standards status: Informative |
The following algorithms are provided for implementors who wish to add advanced functionalities to their PHR implmentations.
This algorithm is intended as a high-level quality control check for longitudinal records assembled from multiple healthcare systems.
The algorithm for normalizing patient identifiers is intended to ensure consistency in patient data across different healthcare systems. This process simplifies the management of a health records, aiming to enhance the accuracy and accessibility of these records.
This algorithm is intended to organize patient records in chronological order. Its main function is to sort various entries, like doctor's notes and test results, by their dates. This makes it easier to view a patient's medical history over time. By putting the records in order, the algorithm helps doctors and healthcare providers track changes and patterns in a patient's health, supporting more informed medical decisions and analysis. This straightforward approach aims to improve patient care by providing a clearer picture of each patient's medical journey.
This algorithm is intended to send out reminders based on a schedule. It uses a clock to keep track of time and automatically triggers alerts for tasks or events at predetermined times. This tool is designed to be straightforward and user-friendly, helping individuals stay organized and on top of their schedules without the need for manual reminders. By providing timely notifications, it assists in managing daily routines and appointments efficiently.
This algorithm is intended to summarize patient records by converting them into a clinical text normal form. After this standardization, it then uses large language models to create a narrative text. This process simplifies complex medical data into a more readable format, making it easier for healthcare professionals to quickly understand a patient's history. The algorithm aims to enhance the efficiency of reviewing patient records by providing clear, concise summaries.
This algorithm is intended to generate reports on gaps in healthcare, specifically focusing on routine procedures like annual physicals or breast/prostate cancer screenings for middle-aged individuals. This tool scans patient records to identify when these important health checks are due or overdue. By highlighting these gaps, the algorithm assists healthcare providers in ensuring that patients receive timely and necessary medical attention. This approach aims to promote preventive healthcare and early detection, contributing to better health outcomes.
This algorithm is intended to act as a software agent that monitors a resource, such as Oxygen usage by a SCUBA diver. This tool mimics a SCUBA diver's dive computer which monitors the amount of air remaining, but applies this concept to other resources that need tracking. The algorithm provides a utilization metric, alerting users as the resource depletes over time. This functionality is particularly useful in scenarios where managing the usage of a limited resource is critical (i.e. remaining medication reserves), ensuring users are aware of the remaining quantity and can plan accordingly.
Deduplication identifies and merges duplicate resources imported from multiple healthcare systems.
Algorithm Definition:
Given sets R₁, R₂, …, Rₙ of FHIR resources from n different sources, find all pairs (rᵢ, rⱼ) where rᵢ ∈ Rₖ, rⱼ ∈ Rₘ, k ≠ m, and similarity(rᵢ, rⱼ) > θ (threshold).
Similarity Function:
similarity(r₁, r₂) = w₁·δ(code₁, code₂) + w₂·τ(date₁, date₂) + w₃·ν(value₁, value₂)
Where:
| τ = temporal proximity (1 - | date₁ - date₂ | /tolerance) |
Resource-Specific Rules:
| Resource | Key Fields | Date Tolerance | Threshold θ |
|---|---|---|---|
| Condition | code, onsetDateTime | ±30 days | 0.85 |
| MedicationStatement | medication.code, effectivePeriod | ±7 days | 0.90 |
| Observation (vitals) | code, effectiveDateTime | ±1 hour | 0.95 |
| Observation (labs) | code, effectiveDateTime | ±1 day | 0.90 |
Academic Pseudocode:
ALGORITHM DeduplicateResources(R: Set of Resources) → Set of ResourcePairs
BEGIN
candidates ← ∅
FOR each r₁ ∈ R DO
FOR each r₂ ∈ R WHERE r₂ ≠ r₁ AND source(r₁) ≠ source(r₂) DO
// Exact identifier match
IF ∃ id ∈ identifiers(r₁) ∩ identifiers(r₂) THEN
candidates ← candidates ∪ {(r₁, r₂, 1.0)}
CONTINUE
END IF
// Fuzzy matching
s ← CalculateSimilarity(r₁, r₂)
IF s > θ THEN
candidates ← candidates ∪ {(r₁, r₂, s)}
END IF
END FOR
END FOR
RETURN SortBySimilarity(candidates)
END
FUNCTION CalculateSimilarity(r₁, r₂: Resource) → ℝ
BEGIN
score ← 0
IF SameCode(r₁, r₂) THEN score ← score + 0.4
IF DateWithinTolerance(r₁, r₂) THEN score ← score + 0.3
IF SameValue(r₁, r₂) THEN score ← score + 0.2
IF SameContext(r₁, r₂) THEN score ← score + 0.1
RETURN score
END
JavaScript + MongoDB Implementation:
async function findDuplicates(db, resourceType, threshold = 0.85) {
const pipeline = [
{
$match: { resourceType: resourceType }
},
{
$lookup: {
from: 'resources',
let: {
code: '$code.coding.code',
date: '$effectiveDateTime',
source: '$meta.source'
},
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$resourceType', resourceType] },
{ $ne: ['$meta.source', '$$source'] },
{ $in: ['$$code', '$code.coding.code'] }
]
}
}
},
{
$addFields: {
dateDiff: {
$abs: {
$subtract: [
{ $toDate: '$effectiveDateTime' },
{ $toDate: '$$date' }
]
}
}
}
},
{
$match: {
dateDiff: { $lt: 3600000 } // 1 hour in ms
}
}
],
as: 'candidates'
}
},
{
$unwind: '$candidates'
},
{
$project: {
_id: 1,
resourceType: 1,
candidate: '$candidates',
similarity: {
$add: [
0.4, // code match
{ $multiply: [0.3,
{ $subtract: [1,
{ $divide: ['$candidates.dateDiff', 86400000] }
]}
]}
]
}
}
},
{
$match: {
similarity: { $gte: threshold }
}
},
{
$sort: { similarity: -1 }
}
];
return await db.collection('resources').aggregate(pipeline).toArray();
}
Identify clinically significant trends in time-series observations.
Mathematical Model:
For observations {(tᵢ, vᵢ)} where i = 1..n, fit linear regression:
v(t) = α + βt + ε
Where β (slope) indicates trend direction and magnitude.
Statistical Significance:
Trend is significant if:
| β | > clinical threshold |
Academic Pseudocode:
ALGORITHM DetectTrend(observations: Array, windowDays: Integer) → TrendResult
BEGIN
// Filter to time window
recent ← FILTER(observations, o.date > now() - windowDays)
// Remove outliers (>3σ)
μ ← MEAN(recent.values)
σ ← STDDEV(recent.values)
cleaned ← FILTER(recent, |o.value - μ| ≤ 3σ)
// Linear regression
n ← LENGTH(cleaned)
x ← [t₁, t₂, ..., tₙ] // timestamps as days
y ← [v₁, v₂, ..., vₙ] // values
x̄ ← MEAN(x)
ȳ ← MEAN(y)
// Calculate slope β
Σxy ← Σᵢ(xᵢ - x̄)(yᵢ - ȳ)
Σx² ← Σᵢ(xᵢ - x̄)²
β ← Σxy / Σx²
α ← ȳ - β·x̄
// Calculate R² and p-value
SSR ← Σᵢ(α + β·xᵢ - ȳ)²
SST ← Σᵢ(yᵢ - ȳ)²
R² ← SSR / SST
SE_β ← √(Σᵢ(yᵢ - α - β·xᵢ)² / ((n-2)·Σx²))
t_stat ← β / SE_β
p_value ← 2·(1 - T_CDF(|t_stat|, n-2))
// Assess clinical significance
IF p_value < 0.05 AND |β| > THRESHOLD AND R² > 0.5 THEN
RETURN TrendResult(
significant: TRUE,
direction: SIGN(β),
rate: β,
confidence: R²,
p_value: p_value
)
ELSE
RETURN TrendResult(significant: FALSE)
END IF
END
JavaScript Implementation:
function analyzeTrend(observations, windowDays = 30, clinicalThreshold = 2) {
// Filter to time window
const cutoff = Date.now() - (windowDays * 86400000);
let recent = observations.filter(o =>
new Date(o.effectiveDateTime).getTime() > cutoff
);
// Remove outliers
const values = recent.map(o => o.valueQuantity.value);
const mean = values.reduce((a,b) => a+b) / values.length;
const stdDev = Math.sqrt(
values.reduce((sq, v) => sq + Math.pow(v - mean, 2), 0) / values.length
);
recent = recent.filter(o =>
Math.abs(o.valueQuantity.value - mean) <= 3 * stdDev
);
// Prepare data for regression
const n = recent.length;
const baseTime = new Date(recent[0].effectiveDateTime).getTime();
const x = recent.map(o =>
(new Date(o.effectiveDateTime).getTime() - baseTime) / 86400000
);
const y = recent.map(o => o.valueQuantity.value);
// Calculate regression
const xMean = x.reduce((a,b) => a+b) / n;
const yMean = y.reduce((a,b) => a+b) / n;
let sumXY = 0, sumX2 = 0;
for (let i = 0; i < n; i++) {
sumXY += (x[i] - xMean) * (y[i] - yMean);
sumX2 += Math.pow(x[i] - xMean, 2);
}
const slope = sumXY / sumX2;
const intercept = yMean - slope * xMean;
// Calculate R²
let SSR = 0, SST = 0;
for (let i = 0; i < n; i++) {
const predicted = intercept + slope * x[i];
SSR += Math.pow(predicted - yMean, 2);
SST += Math.pow(y[i] - yMean, 2);
}
const rSquared = SSR / SST;
// Simplified p-value (t-distribution)
let SSE = 0;
for (let i = 0; i < n; i++) {
SSE += Math.pow(y[i] - intercept - slope * x[i], 2);
}
const seBeta = Math.sqrt(SSE / ((n - 2) * sumX2));
const tStat = slope / seBeta;
const pValue = 2 * (1 - tCDF(Math.abs(tStat), n - 2));
return {
significant: pValue < 0.05 && Math.abs(slope) > clinicalThreshold && rSquared > 0.5,
direction: slope > 0 ? 'increasing' : 'decreasing',
rate: slope,
confidence: rSquared,
pValue: pValue
};
}
Clinical Thresholds:
| Measurement | Concerning Trend |
|---|---|
| Weight | >2 kg/week |
| Systolic BP | >10 mmHg/month |
| Resting HR | >10 bpm/month |
| Fasting glucose | >20 mg/dL/month |
Set Theory Formulation:
Given active medications M = {m₁, m₂, …, mₙ} and interaction database D: M × M → {severity, mechanism}, find all pairs (mᵢ, mⱼ) where D(mᵢ, mⱼ) exists.
Academic Pseudocode:
ALGORITHM CheckInteractions(medications: Set) → Array of Interactions
BEGIN
active ← FILTER(medications, m.status = 'active')
normalized ← MAP(active, m → NormalizeToRxNorm(m))
interactions ← ∅
FOR i ← 1 TO |normalized| - 1 DO
FOR j ← i + 1 TO |normalized| DO
result ← QueryInteractionDB(normalized[i], normalized[j])
IF result ≠ NULL THEN
interactions ← interactions ∪ {result}
END IF
END FOR
END FOR
RETURN SORT(interactions, BY severity DESC)
END
JavaScript + External API:
async function checkMedicationInteractions(medicationStatements) {
const active = medicationStatements.filter(m => m.status === 'active');
// Extract RxNorm codes
const rxnormCodes = active.map(m => {
const coding = m.medicationCodeableConcept?.coding?.find(
c => c.system === 'http://www.nlm.nih.gov/research/umls/rxnorm'
);
return coding?.code;
}).filter(Boolean);
const interactions = [];
// Check all pairs
for (let i = 0; i < rxnormCodes.length - 1; i++) {
for (let j = i + 1; j < rxnormCodes.length; j++) {
const response = await fetch(
`https://rxnav.nlm.nih.gov/REST/interaction/interaction.json?` +
`rxcui=${rxnormCodes[i]}&rxcui=${rxnormCodes[j]}`
);
const data = await response.json();
if (data.interactionTypeGroup) {
data.interactionTypeGroup.forEach(group => {
group.interactionType.forEach(type => {
type.interactionPair.forEach(pair => {
interactions.push({
medication1: pair.interactionConcept[0].minConceptItem.name,
medication2: pair.interactionConcept[1].minConceptItem.name,
severity: pair.severity,
description: pair.description
});
});
});
});
}
}
}
return interactions.sort((a, b) =>
severityRank(b.severity) - severityRank(a.severity)
);
}
function severityRank(severity) {
const ranks = { 'high': 3, 'moderate': 2, 'low': 1 };
return ranks[severity?.toLowerCase()] || 0;
}
Set-Based Definition:
Let G be guidelines, P be patient data. For each guideline g ∈ G:
Academic Pseudocode:
ALGORITHM FindCareGaps(patient: Patient, guidelines: Set) → Set of CareGaps
BEGIN
gaps ← ∅
FOR EACH g ∈ guidelines DO
IF NOT AppliesTo(g, patient) THEN CONTINUE
lastOccurrence ← FindLastOccurrence(patient, g.procedureCodes)
IF lastOccurrence = NULL THEN
gaps ← gaps ∪ {CareGap(
guideline: g,
status: 'never_performed',
priority: 'high'
)}
ELSE IF IsOverdue(lastOccurrence.date, g.interval) THEN
gaps ← gaps ∪ {CareGap(
guideline: g,
status: 'overdue',
lastDate: lastOccurrence.date,
daysOverdue: DaysSince(lastOccurrence.date) - g.interval,
priority: CalculatePriority(daysOverdue)
)}
END IF
END FOR
RETURN SORT(gaps, BY priority DESC, daysOverdue DESC)
END
JavaScript + MongoDB:
const GUIDELINES = [
{
name: 'Colonoscopy',
appliesTo: p => p.age >= 45,
procedureCodes: ['73761001'], // SNOMED
intervalYears: 10
},
{
name: 'Mammogram',
appliesTo: p => p.sex === 'female' && p.age >= 40,
procedureCodes: ['71651007'],
intervalYears: 2
},
{
name: 'Flu Vaccine',
appliesTo: p => true,
procedureCodes: ['86198006'],
intervalYears: 1
},
{
name: 'Lipid Panel',
appliesTo: p => p.age >= 35,
procedureCodes: ['24331-1'], // LOINC
intervalYears: 5
}
];
async function findCareGaps(db, patientId) {
const patient = await db.collection('Patient').findOne({ id: patientId });
const age = calculateAge(patient.birthDate);
patient.age = age;
patient.sex = patient.gender;
const gaps = [];
for (const guideline of GUIDELINES) {
if (!guideline.appliesTo(patient)) continue;
// Find last occurrence
const lastProcedure = await db.collection('Procedure').findOne({
'subject.reference': `Patient/${patientId}`,
'code.coding.code': { $in: guideline.procedureCodes }
}, {
sort: { 'performedDateTime': -1 }
});
if (!lastProcedure) {
gaps.push({
guideline: guideline.name,
status: 'never_performed',
priority: 'high'
});
} else {
const lastDate = new Date(lastProcedure.performedDateTime);
const daysSince = (Date.now() - lastDate.getTime()) / 86400000;
const intervalDays = guideline.intervalYears * 365;
if (daysSince > intervalDays) {
gaps.push({
guideline: guideline.name,
status: 'overdue',
lastDate: lastDate,
daysOverdue: Math.floor(daysSince - intervalDays),
priority: daysSince > intervalDays * 1.5 ? 'high' : 'medium'
});
}
}
}
return gaps.sort((a, b) => {
if (a.priority !== b.priority) {
return a.priority === 'high' ? -1 : 1;
}
return (b.daysOverdue || 0) - (a.daysOverdue || 0);
});
}
function calculateAge(birthDate) {
const today = new Date();
const birth = new Date(birthDate);
let age = today.getFullYear() - birth.getFullYear();
const m = today.getMonth() - birth.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) {
age--;
}
return age;
}
Information Theory Foundation:
Maximize information preservation while minimizing text length:
argmax_S I(R; S) subject to |S| ≤ L
Where:
Academic Pseudocode:
ALGORITHM GenerateSummary(bundle: Bundle, summaryType: String) → String
BEGIN
// Extract key resources
conditions ← ExtractActiveConditions(bundle)
medications ← ExtractActiveMedications(bundle)
vitals ← ExtractRecentVitals(bundle, days: 30)
// Convert to text normal form
conditionsText ← FormatConditions(conditions)
medsText ← FormatMedications(medications)
vitalsText ← FormatVitals(vitals)
// Construct prompt with constraints
prompt ← CONSTRUCT_PROMPT(
type: summaryType,
conditions: conditionsText,
medications: medsText,
vitals: vitalsText,
constraints: [
"Use plain language",
"Highlight concerning trends",
"Do not fabricate",
"Format as 2-3 paragraphs"
]
)
// Generate with safety parameters
response ← LLM_API.generate(
prompt: prompt,
maxTokens: 500,
temperature: 0.3 // Low for factual content
)
// Validate against source
IF ContainsFabrication(response, bundle) THEN
RAISE ValidationError("Summary contains fabricated information")
END IF
RETURN response
END
JavaScript Implementation:
async function generatePatientSummary(bundle, summaryType = 'brief') {
// Extract and normalize
const conditions = bundle.entry
.filter(e => e.resource.resourceType === 'Condition' &&
e.resource.clinicalStatus?.coding?.[0]?.code === 'active')
.map(e => ({
code: e.resource.code.coding[0].display,
onset: e.resource.onsetDateTime
}));
const medications = bundle.entry
.filter(e => e.resource.resourceType === 'MedicationStatement' &&
e.resource.status === 'active')
.map(e => e.resource.medicationCodeableConcept.text);
const recentVitals = bundle.entry
.filter(e => {
if (e.resource.resourceType !== 'Observation') return false;
const date = new Date(e.resource.effectiveDateTime);
return (Date.now() - date.getTime()) < 30 * 86400000;
})
.map(e => ({
type: e.resource.code.coding[0].display,
value: e.resource.valueQuantity.value,
unit: e.resource.valueQuantity.unit,
date: e.resource.effectiveDateTime
}));
// Build prompt
const prompt = `
Generate a ${summaryType} clinical summary for this patient:
Active Conditions: ${conditions.map(c => c.code).join(', ') || 'None documented'}
Current Medications: ${medications.join(', ') || 'None'}
Recent Vitals (last 30 days): ${formatVitals(recentVitals)}
Guidelines:
- Use plain language suitable for patient understanding
- Highlight any concerning trends
- Do not fabricate information not present in the data
- Format as 2-3 paragraphs
- Include disclaimer about AI generation
`;
// Call LLM API (example with OpenAI)
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`
},
body: JSON.stringify({
model: 'gpt-4',
messages: [
{ role: 'system', content: 'You are a medical summarization assistant.' },
{ role: 'user', content: prompt }
],
max_tokens: 500,
temperature: 0.3
})
});
const data = await response.json();
const summary = data.choices[0].message.content;
// Validate (check for hallucinations)
if (containsFabricatedInfo(summary, bundle)) {
throw new Error('Summary contains information not in source data');
}
return summary + '\n\n*This summary was generated by AI and should be reviewed by a healthcare professional.*';
}
function formatVitals(vitals) {
return vitals.map(v => `${v.type}: ${v.value} ${v.unit}`).join('; ');
}
function containsFabricatedInfo(summary, bundle) {
// Simple check: verify mentioned conditions/meds exist in bundle
const allText = JSON.stringify(bundle).toLowerCase();
const summaryLower = summary.toLowerCase();
// Extract medical terms from summary (simplified)
const medicalTerms = summaryLower.match(/\b[a-z]{5,}\b/g) || [];
// Check if unusual terms appear in summary but not in bundle
const suspicious = medicalTerms.filter(term =>
!allText.includes(term) && isMedicalTerm(term)
);
return suspicious.length > 3; // Threshold for concern
}
function isMedicalTerm(word) {
// Simplified check - in production, use medical ontology
const commonMedical = ['diabetes', 'hypertension', 'asthma', 'copd',
'medication', 'blood', 'pressure', 'glucose'];
return commonMedical.includes(word);
}
Safety Considerations: