- Split behavioral inputs into two screens (Habits + Lifestyle) - Added 5 new modifiable factors: diet quality, processed food, drug use, social connection, and stress level - Updated hazard ratios for all new factors based on meta-analyses - Model version bumped to 1.1 - Simplified welcome screen with clearer value proposition - Updated tests for expanded behavioral model Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
199 lines
5.6 KiB
Dart
199 lines
5.6 KiB
Dart
import 'dart:math';
|
|
import '../models/models.dart';
|
|
import 'hazard_ratios.dart';
|
|
import 'mortality_tables.dart';
|
|
|
|
const String modelVersion = '1.1';
|
|
|
|
/// Maximum combined hazard ratio (prevents unrealistic compounding).
|
|
const double _maxCombinedHR = 4.0;
|
|
|
|
/// Damping factor for delta calculation (conservative estimate).
|
|
const double _dampingFactor = 0.3;
|
|
|
|
/// Uncertainty range multipliers (±20% around midpoint).
|
|
const double _lowMultiplier = 0.8;
|
|
const double _highMultiplier = 1.2;
|
|
|
|
/// Calculate combined hazard ratio from behavioral inputs.
|
|
double computeCombinedHazard(BehavioralInputs inputs, double bmi) {
|
|
double hr = 1.0;
|
|
|
|
// Screen 1 factors
|
|
hr *= getSmokingHR(inputs.smoking, inputs.cigarettesPerDay);
|
|
hr *= getAlcoholHR(inputs.alcohol);
|
|
hr *= getSleepHR(inputs.sleepHours, inputs.sleepConsistent);
|
|
hr *= getActivityHR(inputs.activity);
|
|
hr *= getBmiHR(bmi);
|
|
|
|
// Screen 2 factors
|
|
hr *= getDietHR(inputs.diet);
|
|
hr *= getProcessedFoodHR(inputs.processedFood);
|
|
hr *= getDrugUseHR(inputs.drugUse);
|
|
hr *= getSocialHR(inputs.social);
|
|
hr *= getStressHR(inputs.stress);
|
|
hr *= getDrivingHR(inputs.driving);
|
|
hr *= getWorkHoursHR(inputs.workHours);
|
|
|
|
return min(hr, _maxCombinedHR);
|
|
}
|
|
|
|
/// Calculate lifespan delta when modifying a behavior to optimal.
|
|
LifespanDelta _computeDelta(
|
|
double baselineYears,
|
|
double currentHR,
|
|
double modifiedHR,
|
|
String behaviorKey,
|
|
) {
|
|
if (currentHR <= modifiedHR) {
|
|
return LifespanDelta(
|
|
lowMonths: 0,
|
|
highMonths: 0,
|
|
confidence: getConfidenceForBehavior(behaviorKey),
|
|
);
|
|
}
|
|
|
|
final rawDeltaYears =
|
|
baselineYears * (1 - modifiedHR / currentHR) * _dampingFactor;
|
|
|
|
final midpointMonths = rawDeltaYears * 12;
|
|
final lowMonths = (midpointMonths * _lowMultiplier).round();
|
|
final highMonths = (midpointMonths * _highMultiplier).round();
|
|
|
|
return LifespanDelta(
|
|
lowMonths: max(0, lowMonths),
|
|
highMonths: max(0, highMonths),
|
|
confidence: getConfidenceForBehavior(behaviorKey),
|
|
);
|
|
}
|
|
|
|
/// Get modified inputs with a specific behavior set to optimal.
|
|
BehavioralInputs _setToOptimal(BehavioralInputs inputs, String behaviorKey) {
|
|
switch (behaviorKey) {
|
|
case 'smoking':
|
|
return inputs.copyWith(
|
|
smoking: SmokingStatus.never,
|
|
cigarettesPerDay: 0,
|
|
);
|
|
case 'alcohol':
|
|
return inputs.copyWith(alcohol: AlcoholLevel.none);
|
|
case 'sleep':
|
|
return inputs.copyWith(sleepHours: 7.5, sleepConsistent: true);
|
|
case 'activity':
|
|
return inputs.copyWith(activity: ActivityLevel.high);
|
|
case 'diet':
|
|
return inputs.copyWith(diet: DietQuality.excellent);
|
|
case 'processedFood':
|
|
return inputs.copyWith(processedFood: ProcessedFoodLevel.rarely);
|
|
case 'drugUse':
|
|
return inputs.copyWith(drugUse: DrugUse.none);
|
|
case 'social':
|
|
return inputs.copyWith(social: SocialConnection.strong);
|
|
case 'stress':
|
|
return inputs.copyWith(stress: StressLevel.low);
|
|
case 'driving':
|
|
return inputs.copyWith(driving: DrivingExposure.low);
|
|
case 'workHours':
|
|
return inputs.copyWith(workHours: WorkHoursLevel.normal);
|
|
default:
|
|
return inputs;
|
|
}
|
|
}
|
|
|
|
/// Check if a behavior is already at optimal level.
|
|
bool _isOptimal(BehavioralInputs inputs, String behaviorKey) {
|
|
switch (behaviorKey) {
|
|
case 'smoking':
|
|
return inputs.smoking == SmokingStatus.never;
|
|
case 'alcohol':
|
|
return inputs.alcohol == AlcoholLevel.none ||
|
|
inputs.alcohol == AlcoholLevel.light;
|
|
case 'sleep':
|
|
return inputs.sleepHours >= 7 &&
|
|
inputs.sleepHours <= 8 &&
|
|
inputs.sleepConsistent;
|
|
case 'activity':
|
|
return inputs.activity == ActivityLevel.high;
|
|
case 'diet':
|
|
return inputs.diet == DietQuality.excellent;
|
|
case 'processedFood':
|
|
return inputs.processedFood == ProcessedFoodLevel.rarely;
|
|
case 'drugUse':
|
|
return inputs.drugUse == DrugUse.none;
|
|
case 'social':
|
|
return inputs.social == SocialConnection.strong;
|
|
case 'stress':
|
|
return inputs.stress == StressLevel.low;
|
|
case 'driving':
|
|
return inputs.driving == DrivingExposure.low;
|
|
case 'workHours':
|
|
return inputs.workHours == WorkHoursLevel.normal;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// List of modifiable behavior keys.
|
|
const _modifiableBehaviors = [
|
|
'smoking',
|
|
'alcohol',
|
|
'sleep',
|
|
'activity',
|
|
'diet',
|
|
'processedFood',
|
|
'drugUse',
|
|
'social',
|
|
'stress',
|
|
'driving',
|
|
'workHours',
|
|
];
|
|
|
|
/// Calculate ranked factors for a user profile and behavioral inputs.
|
|
CalculationResult calculateRankedFactors(
|
|
UserProfile profile,
|
|
BehavioralInputs inputs,
|
|
) {
|
|
final baselineYears = getRemainingLifeExpectancy(
|
|
profile.age,
|
|
profile.sex,
|
|
profile.country,
|
|
);
|
|
|
|
final conditionHR = getDiagnosisHR(profile.diagnoses);
|
|
final adjustedBaselineYears = baselineYears / conditionHR;
|
|
|
|
final currentHR = computeCombinedHazard(inputs, profile.bmi);
|
|
|
|
final factors = <RankedFactor>[];
|
|
|
|
for (final behaviorKey in _modifiableBehaviors) {
|
|
if (_isOptimal(inputs, behaviorKey)) continue;
|
|
|
|
final modifiedInputs = _setToOptimal(inputs, behaviorKey);
|
|
final modifiedHR = computeCombinedHazard(modifiedInputs, profile.bmi);
|
|
|
|
final delta = _computeDelta(
|
|
adjustedBaselineYears,
|
|
currentHR,
|
|
modifiedHR,
|
|
behaviorKey,
|
|
);
|
|
|
|
if (delta.highMonths >= 1) {
|
|
factors.add(RankedFactor(
|
|
behaviorKey: behaviorKey,
|
|
displayName: getDisplayName(behaviorKey),
|
|
delta: delta,
|
|
));
|
|
}
|
|
}
|
|
|
|
factors.sort(
|
|
(a, b) => b.delta.midpointMonths.compareTo(a.delta.midpointMonths));
|
|
|
|
return CalculationResult(
|
|
rankedFactors: factors,
|
|
modelVersion: modelVersion,
|
|
calculatedAt: DateTime.now(),
|
|
);
|
|
}
|