Local-first Flutter app that identifies the single behavioral change most likely to extend lifespan using hazard-based modeling. Features: - Risk engine with hazard ratios from meta-analyses - 50 countries mapped to 4 mortality groups - 6 modifiable factors: smoking, alcohol, sleep, activity, driving, work hours - SQLite local storage (no cloud, no accounts) - Muted clinical UI theme - 23 unit tests for risk engine Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
176 lines
5.1 KiB
Dart
176 lines
5.1 KiB
Dart
import 'dart:math';
|
||
import '../models/models.dart';
|
||
import 'hazard_ratios.dart';
|
||
import 'mortality_tables.dart';
|
||
|
||
const String modelVersion = '1.0';
|
||
|
||
/// 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.
|
||
const double _lowMultiplier = 0.6;
|
||
const double _highMultiplier = 1.4;
|
||
|
||
/// Calculate combined hazard ratio from behavioral inputs.
|
||
double computeCombinedHazard(BehavioralInputs inputs, double bmi) {
|
||
double hr = 1.0;
|
||
|
||
hr *= getSmokingHR(inputs.smoking, inputs.cigarettesPerDay);
|
||
hr *= getAlcoholHR(inputs.alcohol);
|
||
hr *= getSleepHR(inputs.sleepHours, inputs.sleepConsistent);
|
||
hr *= getActivityHR(inputs.activity);
|
||
hr *= getBmiHR(bmi);
|
||
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) {
|
||
// No improvement possible or already optimal
|
||
return LifespanDelta(
|
||
lowMonths: 0,
|
||
highMonths: 0,
|
||
confidence: getConfidenceForBehavior(behaviorKey),
|
||
);
|
||
}
|
||
|
||
// Delta years ≈ baselineYears × (1 - modifiedHR/currentHR) × dampingFactor
|
||
final rawDeltaYears =
|
||
baselineYears * (1 - modifiedHR / currentHR) * _dampingFactor;
|
||
|
||
// Convert to months with uncertainty range
|
||
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 '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 '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',
|
||
'driving',
|
||
'workHours',
|
||
];
|
||
|
||
/// Calculate ranked factors for a user profile and behavioral inputs.
|
||
CalculationResult calculateRankedFactors(
|
||
UserProfile profile,
|
||
BehavioralInputs inputs,
|
||
) {
|
||
// Get baseline remaining life expectancy
|
||
final baselineYears = getRemainingLifeExpectancy(
|
||
profile.age,
|
||
profile.sex,
|
||
profile.country,
|
||
);
|
||
|
||
// Apply existing condition modifiers (reduces baseline)
|
||
final conditionHR = getDiagnosisHR(profile.diagnoses);
|
||
final adjustedBaselineYears = baselineYears / conditionHR;
|
||
|
||
// Calculate current combined HR from behaviors
|
||
final currentHR = computeCombinedHazard(inputs, profile.bmi);
|
||
|
||
// Calculate delta for each modifiable behavior
|
||
final factors = <RankedFactor>[];
|
||
|
||
for (final behaviorKey in _modifiableBehaviors) {
|
||
// Skip if already optimal
|
||
if (_isOptimal(inputs, behaviorKey)) continue;
|
||
|
||
// Compute HR with this behavior set to optimal
|
||
final modifiedInputs = _setToOptimal(inputs, behaviorKey);
|
||
final modifiedHR = computeCombinedHazard(modifiedInputs, profile.bmi);
|
||
|
||
final delta = _computeDelta(
|
||
adjustedBaselineYears,
|
||
currentHR,
|
||
modifiedHR,
|
||
behaviorKey,
|
||
);
|
||
|
||
// Only include if there's meaningful gain (> 1 month)
|
||
if (delta.highMonths >= 1) {
|
||
factors.add(RankedFactor(
|
||
behaviorKey: behaviorKey,
|
||
displayName: getDisplayName(behaviorKey),
|
||
delta: delta,
|
||
));
|
||
}
|
||
}
|
||
|
||
// Sort by midpoint delta descending
|
||
factors.sort((a, b) => b.delta.midpointMonths.compareTo(a.delta.midpointMonths));
|
||
|
||
return CalculationResult(
|
||
rankedFactors: factors,
|
||
modelVersion: modelVersion,
|
||
calculatedAt: DateTime.now(),
|
||
);
|
||
}
|