|
|
@@ -1,78 +1,103 @@
|
|
|
+import { compose } from '@typed/compose';
|
|
|
import differenceInDays from 'date-fns/differenceInDays';
|
|
|
import addDays from 'date-fns/addDays';
|
|
|
-import { Cases, Data, FutureCase } from '../types';
|
|
|
+import { Case, Cases, Data } from '../types';
|
|
|
|
|
|
// Start the regression line after this many cases have been recorded in total
|
|
|
const regressionStart = 50;
|
|
|
|
|
|
+// Number of days to extrapolate the regression fit on the graph
|
|
|
const futureDays = 10;
|
|
|
|
|
|
const mean = (values: number[]): number =>
|
|
|
values.reduce((last, value) => last + value, 0) / values.length;
|
|
|
|
|
|
-export function getExponentialRegression(cases: Cases): Data {
|
|
|
- if (!cases.length) {
|
|
|
- return [];
|
|
|
- }
|
|
|
+type CaseCumulative = Case & { valueCumulative: number };
|
|
|
|
|
|
- const { index: startIndex } = cases.reduce(
|
|
|
- ({ sum, index }, { value }, nextIndex) => {
|
|
|
- if (sum >= regressionStart) {
|
|
|
- return { sum, index };
|
|
|
- }
|
|
|
-
|
|
|
- const nextSum = sum + value;
|
|
|
- if (nextSum < regressionStart) {
|
|
|
- return { sum: nextSum, index };
|
|
|
- }
|
|
|
+const withNumericDate = (cases: Cases): Data =>
|
|
|
+ cases.map(({ date, ...rest }) => ({
|
|
|
+ date,
|
|
|
+ xValue: date.getTime(),
|
|
|
+ ...rest,
|
|
|
+ }));
|
|
|
|
|
|
- return { sum: nextSum, index: nextIndex };
|
|
|
- },
|
|
|
- { sum: 0, index: -1 },
|
|
|
+const withCumulative = (cases: Data): Data =>
|
|
|
+ cases.reduce(
|
|
|
+ (last: Data, { value = 0, ...rest }): Data => [
|
|
|
+ ...last,
|
|
|
+ {
|
|
|
+ value,
|
|
|
+ valueCumulative: value + (last[last.length - 1]?.valueCumulative || 0),
|
|
|
+ ...rest,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ [],
|
|
|
);
|
|
|
|
|
|
+const logArray = (values: number[]): number[] => values.map(value => Math.log(value));
|
|
|
+
|
|
|
+const withExponentialRegression = (cases: Data): Data => {
|
|
|
+ const startIndex = cases.findIndex(
|
|
|
+ ({ valueCumulative = 0 }) => valueCumulative >= regressionStart,
|
|
|
+ );
|
|
|
if (startIndex === -1) {
|
|
|
- return cases.map(({ date, value }) => ({
|
|
|
- date: date.getTime(),
|
|
|
- value,
|
|
|
- }));
|
|
|
+ return cases;
|
|
|
}
|
|
|
|
|
|
const casesToRegress = cases.slice(startIndex);
|
|
|
|
|
|
// It's assumed that the input here is ordered by date ascending
|
|
|
- const minDate = casesToRegress[0].date;
|
|
|
-
|
|
|
- const xSeries = casesToRegress.map(({ date }) => 1 + differenceInDays(date, minDate));
|
|
|
- const ySeries = casesToRegress.map(({ value }) => Math.log(value));
|
|
|
+ const minDate: Date = new Date(casesToRegress[0].date);
|
|
|
|
|
|
+ const xSeries = casesToRegress.map(({ date }) => 1 + differenceInDays(new Date(date), minDate));
|
|
|
const xBar = mean(xSeries);
|
|
|
- const yBar = mean(ySeries);
|
|
|
+ const xVariance = mean(xSeries.map(value => value ** 2)) - xBar ** 2;
|
|
|
|
|
|
- const covariance =
|
|
|
- xSeries.reduce((last, value, index) => last + (value - xBar) * (ySeries[index] - yBar), 0) /
|
|
|
- xSeries.length;
|
|
|
+ const ySeriesCases = logArray(casesToRegress.map(({ value = 0 }) => value));
|
|
|
+ const ySeriesCumulative = logArray(
|
|
|
+ casesToRegress.map(({ valueCumulative = 0 }) => valueCumulative),
|
|
|
+ );
|
|
|
|
|
|
- const xVariance = mean(xSeries.map(value => value ** 2)) - xBar ** 2;
|
|
|
+ const makeExponentialRegression = (cumulative = false): ((date: Date) => number) => {
|
|
|
+ const ySeries = cumulative ? ySeriesCumulative : ySeriesCases;
|
|
|
+ const yBar = mean(ySeries);
|
|
|
|
|
|
- const slope = covariance / xVariance;
|
|
|
- const intercept = yBar - slope * xBar;
|
|
|
+ const covariance =
|
|
|
+ xSeries.reduce((last, value, index) => last + (value - xBar) * (ySeries[index] - yBar), 0) /
|
|
|
+ xSeries.length;
|
|
|
|
|
|
- const regressionAtDate = (date: Date): number => {
|
|
|
- const xValue = differenceInDays(date, minDate);
|
|
|
+ const slope = covariance / xVariance;
|
|
|
+ const intercept = yBar - slope * xBar;
|
|
|
|
|
|
- return Math.exp(slope * xValue + intercept);
|
|
|
+ return (date: Date): number => Math.exp(slope * differenceInDays(date, minDate) + intercept);
|
|
|
};
|
|
|
|
|
|
+ const regressionAtDate = makeExponentialRegression(false);
|
|
|
+ const regressionAtDateCumulative = makeExponentialRegression(true);
|
|
|
+
|
|
|
const lastDate = cases[cases.length - 1].date;
|
|
|
|
|
|
- const future: FutureCase[] = new Array(futureDays)
|
|
|
+ const future: Data = new Array(futureDays)
|
|
|
.fill(0)
|
|
|
- .map((_, index) => ({ date: addDays(lastDate, index + 1) }));
|
|
|
+ .map((_, index) => addDays(lastDate, index + 1))
|
|
|
+ .map(date => ({ date, xValue: date.getTime() }));
|
|
|
|
|
|
return [...cases, ...future].map(({ date, ...rest }) => ({
|
|
|
- date: date.getTime(),
|
|
|
+ date,
|
|
|
regression: regressionAtDate(date),
|
|
|
+ regressionCumulative: regressionAtDateCumulative(date),
|
|
|
...rest,
|
|
|
}));
|
|
|
+};
|
|
|
+
|
|
|
+export function processCases(cases: Cases): Data {
|
|
|
+ if (!cases.length) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ return compose<Cases, Data, Data, Data>(
|
|
|
+ withExponentialRegression,
|
|
|
+ withCumulative,
|
|
|
+ withNumericDate,
|
|
|
+ )(cases);
|
|
|
}
|