|
@@ -1,7 +1,9 @@
|
|
|
import { compose } from '@typed/compose';
|
|
import { compose } from '@typed/compose';
|
|
|
|
|
+import groupBy from 'lodash/groupBy';
|
|
|
|
|
+import flatten from 'lodash/flatten';
|
|
|
import differenceInDays from 'date-fns/differenceInDays';
|
|
import differenceInDays from 'date-fns/differenceInDays';
|
|
|
import addDays from 'date-fns/addDays';
|
|
import addDays from 'date-fns/addDays';
|
|
|
-import { Case, Cases, Data } from '../types';
|
|
|
|
|
|
|
+import { Cases, Data, DataPoint, CountryCases, CountryDataPoint } from '../types';
|
|
|
|
|
|
|
|
// Start the regression line after this many cases have been recorded in total
|
|
// Start the regression line after this many cases have been recorded in total
|
|
|
const regressionStart = 50;
|
|
const regressionStart = 50;
|
|
@@ -12,31 +14,49 @@ const futureDays = 10;
|
|
|
const mean = (values: number[]): number =>
|
|
const mean = (values: number[]): number =>
|
|
|
values.reduce((last, value) => last + value, 0) / values.length;
|
|
values.reduce((last, value) => last + value, 0) / values.length;
|
|
|
|
|
|
|
|
-type CaseCumulative = Case & { valueCumulative: number };
|
|
|
|
|
|
|
+type WithNumericDate = Omit<CountryDataPoint, 'regression'>[];
|
|
|
|
|
|
|
|
-const withNumericDate = (cases: Cases): Data =>
|
|
|
|
|
- cases.map(({ date, ...rest }) => ({
|
|
|
|
|
|
|
+const withNumericDate = (cases: Cases): WithNumericDate =>
|
|
|
|
|
+ cases.map(({ date, value }) => ({
|
|
|
date,
|
|
date,
|
|
|
xValue: date.getTime(),
|
|
xValue: date.getTime(),
|
|
|
- ...rest,
|
|
|
|
|
|
|
+ value,
|
|
|
}));
|
|
}));
|
|
|
|
|
|
|
|
-const withCumulative = (cases: Data): Data =>
|
|
|
|
|
- cases.reduce(
|
|
|
|
|
- (last: Data, { value = 0, ...rest }): Data => [
|
|
|
|
|
|
|
+type WithCumulative = (Omit<CountryDataPoint, 'regression'> & {
|
|
|
|
|
+ valueCumulative: number;
|
|
|
|
|
+})[];
|
|
|
|
|
+
|
|
|
|
|
+const withCumulative = (cumulative: boolean) => (cases: WithNumericDate): WithCumulative => {
|
|
|
|
|
+ const valuesWithCumulative = cases.reduce(
|
|
|
|
|
+ (last: WithCumulative, { date, xValue, value }): WithCumulative => [
|
|
|
...last,
|
|
...last,
|
|
|
{
|
|
{
|
|
|
|
|
+ date,
|
|
|
|
|
+ xValue,
|
|
|
value,
|
|
value,
|
|
|
- valueCumulative: value + (last[last.length - 1]?.valueCumulative || 0),
|
|
|
|
|
- ...rest,
|
|
|
|
|
|
|
+ valueCumulative: (value || 0) + (last[last.length - 1]?.valueCumulative || 0),
|
|
|
},
|
|
},
|
|
|
],
|
|
],
|
|
|
[],
|
|
[],
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
|
|
+ if (cumulative) {
|
|
|
|
|
+ return valuesWithCumulative.map(({ valueCumulative, value, ...rest }) => ({
|
|
|
|
|
+ ...rest,
|
|
|
|
|
+ value: valueCumulative,
|
|
|
|
|
+ valueCumulative,
|
|
|
|
|
+ }));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return valuesWithCumulative;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
const logArray = (values: number[]): number[] => values.map(value => Math.log(value));
|
|
const logArray = (values: number[]): number[] => values.map(value => Math.log(value));
|
|
|
|
|
|
|
|
-const withExponentialRegression = (cases: Data): Data => {
|
|
|
|
|
|
|
+const withExponentialRegression = (cumulative: boolean) => (
|
|
|
|
|
+ cases: WithCumulative,
|
|
|
|
|
+): CountryDataPoint[] => {
|
|
|
const startIndex = cases.findIndex(
|
|
const startIndex = cases.findIndex(
|
|
|
({ valueCumulative = 0 }) => valueCumulative >= regressionStart,
|
|
({ valueCumulative = 0 }) => valueCumulative >= regressionStart,
|
|
|
);
|
|
);
|
|
@@ -53,31 +73,25 @@ const withExponentialRegression = (cases: Data): Data => {
|
|
|
const xBar = mean(xSeries);
|
|
const xBar = mean(xSeries);
|
|
|
const xVariance = mean(xSeries.map(value => value ** 2)) - xBar ** 2;
|
|
const xVariance = mean(xSeries.map(value => value ** 2)) - xBar ** 2;
|
|
|
|
|
|
|
|
- const ySeriesCases = logArray(casesToRegress.map(({ value = 0 }) => value));
|
|
|
|
|
- const ySeriesCumulative = logArray(
|
|
|
|
|
- casesToRegress.map(({ valueCumulative = 0 }) => valueCumulative),
|
|
|
|
|
|
|
+ const ySeries = logArray(
|
|
|
|
|
+ cumulative
|
|
|
|
|
+ ? casesToRegress.map(({ valueCumulative = 0 }) => valueCumulative)
|
|
|
|
|
+ : casesToRegress.map(({ value = 0 }) => value),
|
|
|
);
|
|
);
|
|
|
|
|
+ const yBar = mean(ySeries);
|
|
|
|
|
+ const covariance =
|
|
|
|
|
+ xSeries.reduce((last, value, index) => last + (value - xBar) * (ySeries[index] - yBar), 0) /
|
|
|
|
|
+ xSeries.length;
|
|
|
|
|
|
|
|
- const makeExponentialRegression = (cumulative = false): ((date: Date) => number) => {
|
|
|
|
|
- const ySeries = cumulative ? ySeriesCumulative : ySeriesCases;
|
|
|
|
|
- const yBar = mean(ySeries);
|
|
|
|
|
-
|
|
|
|
|
- const covariance =
|
|
|
|
|
- xSeries.reduce((last, value, index) => last + (value - xBar) * (ySeries[index] - yBar), 0) /
|
|
|
|
|
- xSeries.length;
|
|
|
|
|
-
|
|
|
|
|
- const slope = covariance / xVariance;
|
|
|
|
|
- const intercept = yBar - slope * xBar;
|
|
|
|
|
-
|
|
|
|
|
- return (date: Date): number => Math.exp(slope * differenceInDays(date, minDate) + intercept);
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ const slope = covariance / xVariance;
|
|
|
|
|
+ const intercept = yBar - slope * xBar;
|
|
|
|
|
|
|
|
- const regressionAtDate = makeExponentialRegression(false);
|
|
|
|
|
- const regressionAtDateCumulative = makeExponentialRegression(true);
|
|
|
|
|
|
|
+ const regressionAtDate = (date: Date): number =>
|
|
|
|
|
+ Math.exp(slope * differenceInDays(date, minDate) + intercept);
|
|
|
|
|
|
|
|
const lastDate = cases[cases.length - 1].date;
|
|
const lastDate = cases[cases.length - 1].date;
|
|
|
|
|
|
|
|
- const future: Data = new Array(futureDays)
|
|
|
|
|
|
|
+ const future: CountryDataPoint[] = new Array(futureDays)
|
|
|
.fill(0)
|
|
.fill(0)
|
|
|
.map((_, index) => addDays(lastDate, index + 1))
|
|
.map((_, index) => addDays(lastDate, index + 1))
|
|
|
.map(date => ({ date, xValue: date.getTime() }));
|
|
.map(date => ({ date, xValue: date.getTime() }));
|
|
@@ -85,19 +99,65 @@ const withExponentialRegression = (cases: Data): Data => {
|
|
|
return [...cases, ...future].map(({ date, ...rest }) => ({
|
|
return [...cases, ...future].map(({ date, ...rest }) => ({
|
|
|
date,
|
|
date,
|
|
|
regression: regressionAtDate(date),
|
|
regression: regressionAtDate(date),
|
|
|
- regressionCumulative: regressionAtDateCumulative(date),
|
|
|
|
|
...rest,
|
|
...rest,
|
|
|
}));
|
|
}));
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-export function processCases(cases: Cases): Data {
|
|
|
|
|
|
|
+function processCountryCases(cases: Cases, cumulative: boolean): CountryDataPoint[] {
|
|
|
if (!cases.length) {
|
|
if (!cases.length) {
|
|
|
return [];
|
|
return [];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return compose<Cases, Data, Data, Data>(
|
|
|
|
|
- withExponentialRegression,
|
|
|
|
|
- withCumulative,
|
|
|
|
|
|
|
+ return compose<Cases, WithNumericDate, WithCumulative, CountryDataPoint[]>(
|
|
|
|
|
+ withExponentialRegression(cumulative),
|
|
|
|
|
+ withCumulative(cumulative),
|
|
|
withNumericDate,
|
|
withNumericDate,
|
|
|
)(cases);
|
|
)(cases);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+function combineData(items: Data[]): Data {
|
|
|
|
|
+ const groups: {
|
|
|
|
|
+ [xValue: string]: Data;
|
|
|
|
|
+ } = groupBy(flatten(items), 'xValue');
|
|
|
|
|
+
|
|
|
|
|
+ return Object.keys(groups)
|
|
|
|
|
+ .sort((timeA: string, timeB: string) => Number(timeA) - Number(timeB))
|
|
|
|
|
+ .map(key =>
|
|
|
|
|
+ groups[key].reduce(
|
|
|
|
|
+ (last: DataPoint, item: DataPoint): DataPoint => ({
|
|
|
|
|
+ ...last,
|
|
|
|
|
+ value: {
|
|
|
|
|
+ ...last.value,
|
|
|
|
|
+ ...item.value,
|
|
|
|
|
+ },
|
|
|
|
|
+ regression: {
|
|
|
|
|
+ ...last.regression,
|
|
|
|
|
+ ...item.regression,
|
|
|
|
|
+ },
|
|
|
|
|
+ }),
|
|
|
|
|
+ {
|
|
|
|
|
+ date: groups[key][0].date,
|
|
|
|
|
+ xValue: groups[key][0].xValue,
|
|
|
|
|
+ value: {},
|
|
|
|
|
+ regression: {},
|
|
|
|
|
+ },
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function processCases(countryCases: CountryCases, cumulative = true): Data {
|
|
|
|
|
+ const data = countryCases.map(({ country, dataSource: { cases } }) =>
|
|
|
|
|
+ processCountryCases(cases, cumulative).map(({ date, xValue, value, regression }) => ({
|
|
|
|
|
+ date,
|
|
|
|
|
+ xValue,
|
|
|
|
|
+ value: {
|
|
|
|
|
+ [country]: value,
|
|
|
|
|
+ },
|
|
|
|
|
+ regression: {
|
|
|
|
|
+ [country]: regression,
|
|
|
|
|
+ },
|
|
|
|
|
+ })),
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ return combineData(data);
|
|
|
|
|
+}
|