Spaces:
Running
Running
optim
Browse files- src/utils/metricAcwr.ts +145 -41
src/utils/metricAcwr.ts
CHANGED
|
@@ -11,8 +11,8 @@ function formatDateLocal(date: Date): string {
|
|
| 11 |
}
|
| 12 |
|
| 13 |
/**
|
| 14 |
-
* Calculate optimal activity values for the next 7 days to
|
| 15 |
-
* Uses
|
| 16 |
* @param allDates - Array of all historical dates
|
| 17 |
* @param dailyValues - Map of date strings to activity values
|
| 18 |
* @param targetACWR - Target ACWR to achieve
|
|
@@ -32,93 +32,197 @@ function calculateOptimalFutureDays(
|
|
| 32 |
futureAverage28d: number[];
|
| 33 |
futureAcwr: number[];
|
| 34 |
} {
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
const
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
const simulatedDailyValues = new Map(dailyValues);
|
| 43 |
const simulatedAllDates = [...allDates];
|
| 44 |
-
|
| 45 |
-
// Get the last date in the data
|
| 46 |
const lastDate = new Date(allDates[allDates.length - 1]);
|
| 47 |
|
| 48 |
for (let dayOffset = 0; dayOffset < numFutureDays; dayOffset++) {
|
| 49 |
const futureDate = new Date(lastDate);
|
| 50 |
-
// Add 1 + startOffset + dayOffset to go beyond lastDate
|
| 51 |
-
// startOffset=0: start from day after lastDate (tomorrow if lastDate is today)
|
| 52 |
-
// startOffset=1: skip one day, start from 2 days after lastDate
|
| 53 |
futureDate.setDate(lastDate.getDate() + 1 + startOffset + dayOffset);
|
| 54 |
const futureDateStr = formatDateLocal(futureDate);
|
| 55 |
|
| 56 |
-
futureDates.push(futureDateStr);
|
| 57 |
simulatedAllDates.push(futureDate);
|
| 58 |
-
|
| 59 |
const currentIndex = simulatedAllDates.length - 1;
|
| 60 |
|
| 61 |
-
// Calculate
|
| 62 |
let acuteSum = 0;
|
| 63 |
for (let i = Math.max(0, currentIndex - 6); i < currentIndex; i++) {
|
| 64 |
const checkDateStr = formatDateLocal(simulatedAllDates[i]);
|
| 65 |
acuteSum += simulatedDailyValues.get(checkDateStr) || 0;
|
| 66 |
}
|
| 67 |
|
| 68 |
-
// Calculate what the 28-day sum would be (last 27 days + today)
|
| 69 |
let chronicSum = 0;
|
| 70 |
for (let i = Math.max(0, currentIndex - 27); i < currentIndex; i++) {
|
| 71 |
const checkDateStr = formatDateLocal(simulatedAllDates[i]);
|
| 72 |
chronicSum += simulatedDailyValues.get(checkDateStr) || 0;
|
| 73 |
}
|
| 74 |
|
| 75 |
-
//
|
| 76 |
-
// targetACWR = [(acuteSum + X) / 7] / [(chronicSum + X) / 28]
|
| 77 |
-
// Solving for X:
|
| 78 |
-
// X = (4 * acuteSum - targetACWR * chronicSum) / (targetACWR - 4)
|
| 79 |
-
|
| 80 |
const numerator = 4 * acuteSum - targetACWR * chronicSum;
|
| 81 |
const denominator = targetACWR - 4;
|
| 82 |
-
|
| 83 |
let optimalValue = 0;
|
| 84 |
|
| 85 |
if (Math.abs(denominator) > 0.001) {
|
| 86 |
optimalValue = numerator / denominator;
|
| 87 |
-
|
| 88 |
-
// Ensure non-negative values (can't have negative activity)
|
| 89 |
-
if (optimalValue < 0) {
|
| 90 |
-
optimalValue = 0;
|
| 91 |
-
}
|
| 92 |
} else {
|
| 93 |
-
|
| 94 |
-
// In this case, set to average of recent values to maintain stability
|
| 95 |
-
const recentSum = acuteSum;
|
| 96 |
-
optimalValue = recentSum / 7;
|
| 97 |
}
|
| 98 |
|
| 99 |
-
|
| 100 |
-
futureValues.push(optimalValue);
|
| 101 |
simulatedDailyValues.set(futureDateStr, optimalValue);
|
|
|
|
| 102 |
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
let newAcuteSum = 0;
|
| 105 |
for (let i = Math.max(0, currentIndex - 6); i <= currentIndex; i++) {
|
| 106 |
-
const checkDateStr = formatDateLocal(
|
| 107 |
-
newAcuteSum +=
|
| 108 |
}
|
| 109 |
const newAcuteAvg = newAcuteSum / 7;
|
| 110 |
futureAverage7d.push(newAcuteAvg);
|
| 111 |
|
| 112 |
-
// Calculate
|
| 113 |
let newChronicSum = 0;
|
| 114 |
for (let i = Math.max(0, currentIndex - 27); i <= currentIndex; i++) {
|
| 115 |
-
const checkDateStr = formatDateLocal(
|
| 116 |
-
newChronicSum +=
|
| 117 |
}
|
| 118 |
const newChronicAvg = newChronicSum / 28;
|
| 119 |
futureAverage28d.push(newChronicAvg);
|
| 120 |
|
| 121 |
-
// Calculate
|
| 122 |
const newACWR = newChronicAvg > 0 ? newAcuteAvg / newChronicAvg : 0;
|
| 123 |
futureAcwr.push(newACWR);
|
| 124 |
}
|
|
|
|
| 11 |
}
|
| 12 |
|
| 13 |
/**
|
| 14 |
+
* Calculate optimal activity values for the next 7 days to minimize MSE between ACWR and target
|
| 15 |
+
* Uses optimization to find values that maintain ACWR close to target across all predicted days
|
| 16 |
* @param allDates - Array of all historical dates
|
| 17 |
* @param dailyValues - Map of date strings to activity values
|
| 18 |
* @param targetACWR - Target ACWR to achieve
|
|
|
|
| 32 |
futureAverage28d: number[];
|
| 33 |
futureAcwr: number[];
|
| 34 |
} {
|
| 35 |
+
// Variance weight parameter (0-1): controls trade-off between ACWR accuracy and value consistency
|
| 36 |
+
// Higher weight = more preference for consistent daily values
|
| 37 |
+
const varianceWeight = 0.5;
|
| 38 |
+
|
| 39 |
+
// Helper function to calculate variance of predicted values
|
| 40 |
+
const calculateVariance = (values: number[]): number => {
|
| 41 |
+
if (values.length === 0) return 0;
|
| 42 |
+
const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
|
| 43 |
+
const variance = values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / values.length;
|
| 44 |
+
return variance;
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
// Helper function to simulate predictions and calculate error (MSE + variance penalty)
|
| 48 |
+
const simulateAndCalculateError = (values: number[]): { error: number; acwrValues: number[]; variance: number } => {
|
| 49 |
+
const simulatedDailyValues = new Map(dailyValues);
|
| 50 |
+
const simulatedAllDates = [...allDates];
|
| 51 |
+
const lastDate = new Date(allDates[allDates.length - 1]);
|
| 52 |
+
const acwrValues: number[] = [];
|
| 53 |
+
|
| 54 |
+
for (let dayOffset = 0; dayOffset < numFutureDays; dayOffset++) {
|
| 55 |
+
const futureDate = new Date(lastDate);
|
| 56 |
+
futureDate.setDate(lastDate.getDate() + 1 + startOffset + dayOffset);
|
| 57 |
+
const futureDateStr = formatDateLocal(futureDate);
|
| 58 |
+
|
| 59 |
+
simulatedDailyValues.set(futureDateStr, values[dayOffset]);
|
| 60 |
+
simulatedAllDates.push(futureDate);
|
| 61 |
+
|
| 62 |
+
const currentIndex = simulatedAllDates.length - 1;
|
| 63 |
+
|
| 64 |
+
// Calculate 7-day average
|
| 65 |
+
let acuteSum = 0;
|
| 66 |
+
for (let i = Math.max(0, currentIndex - 6); i <= currentIndex; i++) {
|
| 67 |
+
const checkDateStr = formatDateLocal(simulatedAllDates[i]);
|
| 68 |
+
acuteSum += simulatedDailyValues.get(checkDateStr) || 0;
|
| 69 |
+
}
|
| 70 |
+
const acuteAvg = acuteSum / 7;
|
| 71 |
+
|
| 72 |
+
// Calculate 28-day average
|
| 73 |
+
let chronicSum = 0;
|
| 74 |
+
for (let i = Math.max(0, currentIndex - 27); i <= currentIndex; i++) {
|
| 75 |
+
const checkDateStr = formatDateLocal(simulatedAllDates[i]);
|
| 76 |
+
chronicSum += simulatedDailyValues.get(checkDateStr) || 0;
|
| 77 |
+
}
|
| 78 |
+
const chronicAvg = chronicSum / 28;
|
| 79 |
+
|
| 80 |
+
const acwr = chronicAvg > 0 ? acuteAvg / chronicAvg : 0;
|
| 81 |
+
acwrValues.push(acwr);
|
| 82 |
+
}
|
| 83 |
|
| 84 |
+
// Calculate MSE for ACWR
|
| 85 |
+
let mse = 0;
|
| 86 |
+
for (const acwr of acwrValues) {
|
| 87 |
+
const diff = acwr - targetACWR;
|
| 88 |
+
mse += diff * diff;
|
| 89 |
+
}
|
| 90 |
+
mse /= numFutureDays;
|
| 91 |
+
|
| 92 |
+
// Calculate variance of predicted values
|
| 93 |
+
const variance = calculateVariance(values);
|
| 94 |
+
|
| 95 |
+
// Normalize variance by mean to make it scale-independent
|
| 96 |
+
const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
|
| 97 |
+
const normalizedVariance = mean > 0 ? variance / (mean * mean) : 0;
|
| 98 |
+
|
| 99 |
+
// Combined error: MSE + weighted variance penalty
|
| 100 |
+
const error = mse + varianceWeight * normalizedVariance;
|
| 101 |
+
|
| 102 |
+
return { error, acwrValues, variance };
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
+
// Initialize with greedy solution as starting point
|
| 106 |
+
const initialValues: number[] = [];
|
| 107 |
const simulatedDailyValues = new Map(dailyValues);
|
| 108 |
const simulatedAllDates = [...allDates];
|
|
|
|
|
|
|
| 109 |
const lastDate = new Date(allDates[allDates.length - 1]);
|
| 110 |
|
| 111 |
for (let dayOffset = 0; dayOffset < numFutureDays; dayOffset++) {
|
| 112 |
const futureDate = new Date(lastDate);
|
|
|
|
|
|
|
|
|
|
| 113 |
futureDate.setDate(lastDate.getDate() + 1 + startOffset + dayOffset);
|
| 114 |
const futureDateStr = formatDateLocal(futureDate);
|
| 115 |
|
|
|
|
| 116 |
simulatedAllDates.push(futureDate);
|
|
|
|
| 117 |
const currentIndex = simulatedAllDates.length - 1;
|
| 118 |
|
| 119 |
+
// Calculate sums for greedy solution
|
| 120 |
let acuteSum = 0;
|
| 121 |
for (let i = Math.max(0, currentIndex - 6); i < currentIndex; i++) {
|
| 122 |
const checkDateStr = formatDateLocal(simulatedAllDates[i]);
|
| 123 |
acuteSum += simulatedDailyValues.get(checkDateStr) || 0;
|
| 124 |
}
|
| 125 |
|
|
|
|
| 126 |
let chronicSum = 0;
|
| 127 |
for (let i = Math.max(0, currentIndex - 27); i < currentIndex; i++) {
|
| 128 |
const checkDateStr = formatDateLocal(simulatedAllDates[i]);
|
| 129 |
chronicSum += simulatedDailyValues.get(checkDateStr) || 0;
|
| 130 |
}
|
| 131 |
|
| 132 |
+
// Greedy optimal value
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
const numerator = 4 * acuteSum - targetACWR * chronicSum;
|
| 134 |
const denominator = targetACWR - 4;
|
|
|
|
| 135 |
let optimalValue = 0;
|
| 136 |
|
| 137 |
if (Math.abs(denominator) > 0.001) {
|
| 138 |
optimalValue = numerator / denominator;
|
| 139 |
+
if (optimalValue < 0) optimalValue = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
} else {
|
| 141 |
+
optimalValue = acuteSum / 7;
|
|
|
|
|
|
|
|
|
|
| 142 |
}
|
| 143 |
|
| 144 |
+
initialValues.push(optimalValue);
|
|
|
|
| 145 |
simulatedDailyValues.set(futureDateStr, optimalValue);
|
| 146 |
+
}
|
| 147 |
|
| 148 |
+
// Iterative refinement to minimize error (MSE + variance penalty)
|
| 149 |
+
let bestValues = [...initialValues];
|
| 150 |
+
let bestError = simulateAndCalculateError(bestValues).error;
|
| 151 |
+
|
| 152 |
+
// Simple gradient descent-like optimization
|
| 153 |
+
const iterations = 50;
|
| 154 |
+
const learningRate = 0.3;
|
| 155 |
+
|
| 156 |
+
for (let iter = 0; iter < iterations; iter++) {
|
| 157 |
+
const currentResult = simulateAndCalculateError(bestValues);
|
| 158 |
+
|
| 159 |
+
// Try adjusting each day's value
|
| 160 |
+
for (let dayIdx = 0; dayIdx < numFutureDays; dayIdx++) {
|
| 161 |
+
const originalValue = bestValues[dayIdx];
|
| 162 |
+
|
| 163 |
+
// Calculate gradient by finite difference
|
| 164 |
+
const delta = Math.max(1, originalValue * 0.1);
|
| 165 |
+
const testValues = [...bestValues];
|
| 166 |
+
testValues[dayIdx] = originalValue + delta;
|
| 167 |
+
const upResult = simulateAndCalculateError(testValues);
|
| 168 |
+
|
| 169 |
+
const gradient = (upResult.error - currentResult.error) / delta;
|
| 170 |
+
|
| 171 |
+
// Update value using gradient
|
| 172 |
+
let newValue = originalValue - learningRate * gradient * originalValue;
|
| 173 |
+
newValue = Math.max(0, newValue); // Ensure non-negative
|
| 174 |
+
|
| 175 |
+
// Test if this improves error
|
| 176 |
+
testValues[dayIdx] = newValue;
|
| 177 |
+
const newResult = simulateAndCalculateError(testValues);
|
| 178 |
+
|
| 179 |
+
if (newResult.error < bestError) {
|
| 180 |
+
bestValues[dayIdx] = newValue;
|
| 181 |
+
bestError = newResult.error;
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
// Build final results with optimized values
|
| 187 |
+
const futureDates: string[] = [];
|
| 188 |
+
const futureValues: number[] = bestValues;
|
| 189 |
+
const futureAverage7d: number[] = [];
|
| 190 |
+
const futureAverage28d: number[] = [];
|
| 191 |
+
const futureAcwr: number[] = [];
|
| 192 |
+
|
| 193 |
+
const finalSimulatedDailyValues = new Map(dailyValues);
|
| 194 |
+
const finalSimulatedAllDates = [...allDates];
|
| 195 |
+
|
| 196 |
+
for (let dayOffset = 0; dayOffset < numFutureDays; dayOffset++) {
|
| 197 |
+
const futureDate = new Date(lastDate);
|
| 198 |
+
futureDate.setDate(lastDate.getDate() + 1 + startOffset + dayOffset);
|
| 199 |
+
const futureDateStr = formatDateLocal(futureDate);
|
| 200 |
+
|
| 201 |
+
futureDates.push(futureDateStr);
|
| 202 |
+
finalSimulatedDailyValues.set(futureDateStr, bestValues[dayOffset]);
|
| 203 |
+
finalSimulatedAllDates.push(futureDate);
|
| 204 |
+
|
| 205 |
+
const currentIndex = finalSimulatedAllDates.length - 1;
|
| 206 |
+
|
| 207 |
+
// Calculate 7-day average
|
| 208 |
let newAcuteSum = 0;
|
| 209 |
for (let i = Math.max(0, currentIndex - 6); i <= currentIndex; i++) {
|
| 210 |
+
const checkDateStr = formatDateLocal(finalSimulatedAllDates[i]);
|
| 211 |
+
newAcuteSum += finalSimulatedDailyValues.get(checkDateStr) || 0;
|
| 212 |
}
|
| 213 |
const newAcuteAvg = newAcuteSum / 7;
|
| 214 |
futureAverage7d.push(newAcuteAvg);
|
| 215 |
|
| 216 |
+
// Calculate 28-day average
|
| 217 |
let newChronicSum = 0;
|
| 218 |
for (let i = Math.max(0, currentIndex - 27); i <= currentIndex; i++) {
|
| 219 |
+
const checkDateStr = formatDateLocal(finalSimulatedAllDates[i]);
|
| 220 |
+
newChronicSum += finalSimulatedDailyValues.get(checkDateStr) || 0;
|
| 221 |
}
|
| 222 |
const newChronicAvg = newChronicSum / 28;
|
| 223 |
futureAverage28d.push(newChronicAvg);
|
| 224 |
|
| 225 |
+
// Calculate ACWR
|
| 226 |
const newACWR = newChronicAvg > 0 ? newAcuteAvg / newChronicAvg : 0;
|
| 227 |
futureAcwr.push(newACWR);
|
| 228 |
}
|