Health Scoring System - Detailed Algorithms
Overview
This document provides detailed scoring algorithms that extend the health scoring implementation in service-architecture.md. The LHR health scoring system uses a 0-100 scale with four weighted components.
Component Scoring Algorithms
1. Vital Signs Score (25% weight)
Purpose: Assess physiological health based on recent vital sign measurements
def calculate_vital_score(patient)
recent_vitals = patient.patient_vitals.recent.limit(10)
return 85.0 if recent_vitals.empty? # Default score if no vitals
normal_count = 0
total_measurements = 0
recent_vitals.each do |vital|
# Blood Pressure Assessment
if vital.systolic_bp.present? && vital.diastolic_bp.present?
total_measurements += 1
if vital_bp_normal?(vital.systolic_bp, vital.diastolic_bp)
normal_count += 1
end
end
# Heart Rate Assessment
if vital.heart_rate.present?
total_measurements += 1
if vital_hr_normal?(vital.heart_rate)
normal_count += 1
end
end
# Temperature Assessment
if vital.temperature.present?
total_measurements += 1
if vital_temp_normal?(vital.temperature)
normal_count += 1
end
end
end
return 85.0 if total_measurements == 0
base_score = (normal_count.to_f / total_measurements * 100).round(2)
# Bonus for recent measurements (within 30 days)
recent_bonus = recent_vitals.where('created_at > ?', 30.days.ago).any? ? 5 : 0
final_score = base_score + recent_bonus
[[final_score, 100].min, 0].max.round(2)
end
private
def vital_bp_normal?(systolic, diastolic)
# Normal BP: Systolic 90-130, Diastolic 60-85
systolic.between?(90, 130) && diastolic.between?(60, 85)
end
def vital_hr_normal?(heart_rate)
# Normal heart rate: 60-100 bpm
heart_rate.between?(60, 100)
end
def vital_temp_normal?(temperature)
# Normal temperature: 97.0-99.5°F (36.1-37.5°C)
if temperature > 35 && temperature < 40 # Celsius
temperature.between?(36.1, 37.5)
else # Fahrenheit
temperature.between?(97.0, 99.5)
end
end2. Problem Severity Score (30% weight)
Purpose: Assess health impact of active medical problems
def calculate_problem_score(patient)
active_problems = patient.encounter_complaints.where(problem_status: ['active', 'chronic'])
base_score = 100.0
# Deduct points based on problem severity and type
active_problems.each do |problem|
deduction = calculate_problem_deduction(problem)
base_score -= deduction
end
# Additional penalties for problem patterns
chronic_penalty = calculate_chronic_condition_penalty(active_problems)
unresolved_penalty = calculate_unresolved_problem_penalty(active_problems)
final_score = base_score - chronic_penalty - unresolved_penalty
[[final_score, 100].min, 0].max.round(2)
end
private
def calculate_problem_deduction(problem)
# Base deduction by severity
severity_deduction = case problem.severity_level
when 'critical' then 25
when 'severe' then 20
when 'moderate' then 12
when 'mild' then 6
else 10 # unknown severity
end
# Multipliers for problem characteristics
multiplier = 1.0
# Chronic conditions have greater impact
multiplier *= 1.3 if problem.chronic_condition?
# Long-standing problems without resolution
if problem.onset_date.present?
days_active = (Date.current - problem.onset_date).to_i
if days_active > 365 # More than 1 year
multiplier *= 1.2
elsif days_active > 90 # More than 3 months
multiplier *= 1.1
end
end
# Problems affecting health score calculation
multiplier *= 1.1 if problem.affects_health_score?
(severity_deduction * multiplier).round(2)
end
def calculate_chronic_condition_penalty(problems)
chronic_count = problems.select(&:chronic_condition?).count
# Progressive penalty for multiple chronic conditions
case chronic_count
when 0 then 0
when 1 then 2
when 2 then 5
when 3 then 10
else 15 # 4+ chronic conditions
end
end
def calculate_unresolved_problem_penalty(problems)
old_unresolved = problems.select do |p|
p.onset_date.present? &&
p.onset_date < 6.months.ago &&
p.problem_status == 'active'
end
old_unresolved.count * 3 # 3 points per old unresolved problem
end3. Medication Adherence Score (25% weight)
Purpose: Assess medication management and safety
def calculate_medication_score(patient)
active_medications = patient.encounter_medicines.where(medication_status: 'active')
return 95.0 if active_medications.empty? # Good score if no medications needed
base_score = 90.0 # Start with high baseline
# Medication management factors
chronic_medication_bonus = calculate_chronic_medication_bonus(active_medications)
interaction_penalty = calculate_interaction_penalty(active_medications)
# Note: adherence bonus removed for MVP - will be added in future phases
adherence_bonus = 0
complexity_penalty = calculate_medication_complexity_penalty(active_medications)
final_score = base_score + chronic_medication_bonus + adherence_bonus - interaction_penalty - complexity_penalty
[[final_score, 100].min, 0].max.round(2)
end
private
def calculate_chronic_medication_bonus(medications)
chronic_meds = medications.where(prescription_type: 'chronic')
# Bonus for properly tracking chronic medications
if chronic_meds.count > 0
5 # Bonus for having chronic medications tracked
else
0 # No bonus if no chronic medications
end
end
def calculate_interaction_penalty(medications)
interactions = []
medications.each do |med|
if med.drug_interactions.present? && med.drug_interactions.any?
interactions.concat(med.drug_interactions)
end
end
# Penalty based on interaction severity
penalty = 0
interactions.each do |interaction|
case interaction['severity']
when 'high' then penalty += 8
when 'moderate' then penalty += 5
when 'low' then penalty += 2
end
end
penalty
end
# Note: calculate_adherence_bonus method removed for MVP - adherence tracking will be added in future phases
def calculate_medication_complexity_penalty(medications)
# Penalty for high medication burden
med_count = medications.count
case med_count
when 0..2 then 0
when 3..5 then 2
when 6..8 then 5
when 9..12 then 8
else 12 # 13+ medications
end
end4. Preventive Care Score (20% weight)
Purpose: Assess engagement with preventive healthcare measures
def calculate_preventive_care_score(patient)
age = patient.patient_age
gender = patient.gender&.downcase
# Age-based baseline expectations
baseline_score = age_based_preventive_baseline(age)
# Specific preventive care assessments
immunization_score = calculate_immunization_score(patient, age)
screening_score = calculate_screening_score(patient, age, gender)
wellness_visit_score = calculate_wellness_visit_score(patient)
# Weighted combination
final_score = (
baseline_score * 0.4 +
immunization_score * 0.3 +
screening_score * 0.2 +
wellness_visit_score * 0.1
)
[[final_score, 100].min, 0].max.round(2)
end
private
def age_based_preventive_baseline(age)
case age
when 0..2 then 90 # Infants - high preventive care engagement expected
when 3..17 then 85 # Children - good preventive care expected
when 18..25 then 70 # Young adults - often skip preventive care
when 26..40 then 75 # Adults - moderate preventive care
when 41..65 then 80 # Middle age - higher health awareness
else 85 # Seniors - typically good healthcare engagement
end
end
def calculate_immunization_score(patient, age)
# Simplified immunization scoring
recent_immunizations = patient.patient_immunizations
.where('created_at > ?', 1.year.ago)
.count
if recent_immunizations >= 1
90 # Has recent immunizations
else
60 # No recent immunizations
end
end
def calculate_screening_score(patient, age, gender)
# Basic screening score
75 # Default score - actual implementation would check screening records
end
def calculate_wellness_visit_score(patient)
recent_encounters = patient.encounters
.where('created_at > ?', 1.year.ago)
.where(encounter_type: ['routine', 'followup'])
.count
case recent_encounters
when 0 then 60 # No wellness visits
when 1 then 80 # One wellness visit
else 90 # Good engagement
end
endHealth Score Trending
Trend Analysis
def calculate_health_score_trend(patient, period_months = 6)
scores = patient.patient_health_scores
.where('calculated_date >= ?', period_months.months.ago)
.order(:calculated_date)
return 'insufficient_data' if scores.count < 2
# Simple trend based on first and last scores
first_score = scores.first.overall_score
last_score = scores.last.overall_score
change = last_score - first_score
# Categorize trend
if change > 5
'improving'
elsif change < -5
'declining'
else
'stable'
end
endHealth Score Interpretation
Clinical Risk Stratification
def interpret_health_score(score)
interpretation = {
score: score,
category: score_category(score),
risk_level: risk_level(score),
recommendations: generate_recommendations(score),
alert_level: alert_level(score)
}
end
private
def score_category(score)
case score
when 90..100 then 'excellent'
when 80..89 then 'good'
when 70..79 then 'fair'
when 60..69 then 'poor'
else 'critical'
end
end
def risk_level(score)
case score
when 90..100 then 'low'
when 70..89 then 'moderate'
when 50..69 then 'high'
else 'very_high'
end
end
def generate_recommendations(score)
recommendations = []
if score < 60
recommendations << 'Immediate clinical review recommended'
recommendations << 'Consider care coordination with specialists'
elsif score < 70
recommendations << 'Enhanced monitoring and follow-up needed'
recommendations << 'Review medication adherence and problem management'
elsif score < 80
recommendations << 'Focus on preventive care and lifestyle modifications'
recommendations << 'Regular monitoring recommended'
else
recommendations << 'Continue current care approach'
recommendations << 'Maintain preventive care schedule'
end
recommendations
end
def alert_level(score)
case score
when 0..49 then 'critical'
when 50..59 then 'high'
when 60..69 then 'medium'
else 'low'
end
endKey Algorithms Provided:
✅ Vital Signs Scoring: Normal range assessments for BP, heart rate, temperature
✅ Problem Severity Calculation: Deduction-based scoring with chronic condition factors
✅ Medication Safety Scoring: Interaction penalties and chronic medication tracking
✅ Preventive Care Assessment: Simplified immunization and wellness visit tracking
✅ Score Interpretation: Category thresholds and risk levels
Note: This document complements the health scoring implementation in service-architecture.md by providing detailed algorithms for specific calculations.