Ver Fonte

Cumulative cases with exponential regression

Fela Maslen há 5 anos atrás
pai
commit
682fad7857
5 ficheiros alterados com 88 adições e 50 exclusões
  1. 1 0
      package.json
  2. 14 6
      src/components/graph-cases.tsx
  3. 4 5
      src/types.ts
  4. 64 39
      src/utils/regression.ts
  5. 5 0
      yarn.lock

+ 1 - 0
package.json

@@ -6,6 +6,7 @@
     "@testing-library/jest-dom": "^4.2.4",
     "@testing-library/react": "^9.3.2",
     "@testing-library/user-event": "^7.1.2",
+    "@typed/compose": "^1.0.0",
     "@types/jest": "^24.0.0",
     "@types/node": "^12.0.0",
     "@types/react": "^16.9.0",

+ 14 - 6
src/components/graph-cases.tsx

@@ -4,7 +4,7 @@ import { LineChart, XAxis, YAxis, Line, CartesianGrid } from 'recharts';
 
 import { Cases, Country, Data } from '../types';
 import { getCases } from '../utils/get-cases';
-import { getExponentialRegression } from '../utils/regression';
+import { processCases } from '../utils/regression';
 
 type Props = {
   country: Country;
@@ -23,22 +23,30 @@ const dateTickFormatter = (date: Date): string => {
 
 const GraphCases: React.FC<Props> = ({ country }) => {
   const cases = React.useMemo<Cases>(() => getCases(country), [country]);
-  const data = React.useMemo<Data>(() => getExponentialRegression(cases), [cases]);
+  const data = React.useMemo<Data>(() => processCases(cases), [cases]);
 
   return (
     <div>
       <h3>Country: {country.toUpperCase()}</h3>
       <LineChart width={640} height={480} data={data} margin={margin}>
         <XAxis
-          dataKey="date"
+          dataKey="xValue"
           domain={['auto', 'auto']}
           tickFormatter={dateTickFormatter}
           type="number"
         />
-        <YAxis tick />
+        <YAxis tick yAxisId="left" />
         <CartesianGrid stroke="#f5f5f5" />
-        <Line type="monotone" dataKey="value" stroke="#000" yAxisId={0} />
-        <Line type="monotone" dataKey="regression" stroke="#06c" yAxisId={0} dot={false} />
+        <Line type="monotone" dataKey="value" stroke="#ffabf8" yAxisId="left" />
+        <Line type="monotone" dataKey="regression" stroke="#ffcffb" yAxisId="left" dot={false} />
+        <Line type="monotone" dataKey="valueCumulative" stroke="#cc3d55" yAxisId="left" />
+        <Line
+          type="monotone"
+          dataKey="regressionCumulative"
+          stroke="#e86178"
+          yAxisId="left"
+          dot={false}
+        />
       </LineChart>
     </div>
   );

+ 4 - 5
src/types.ts

@@ -5,14 +5,13 @@ export type Case = {
 
 export type Cases = Case[];
 
-export type FutureCase = {
-  date: Date;
-};
-
 export type Data = {
-  date: number;
+  date: Date;
+  xValue: number;
   value?: number;
+  valueCumulative?: number;
   regression?: number;
+  regressionCumulative?: number;
 }[];
 
 export type Country = 'uk';

+ 64 - 39
src/utils/regression.ts

@@ -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);
 }

+ 5 - 0
yarn.lock

@@ -1267,6 +1267,11 @@
   resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-7.2.1.tgz#2ad4e844175a3738cb9e7064be5ea070b8863a1c"
   integrity sha512-oZ0Ib5I4Z2pUEcoo95cT1cr6slco9WY7yiPpG+RGNkj8YcYgJnM7pXmYmorNOReh8MIGcKSqXyeGjxnr8YiZbA==
 
+"@typed/compose@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@typed/compose/-/compose-1.0.0.tgz#e4e5fc7132b84c701acb5346a361da28fad63081"
+  integrity sha1-5OX8cTK4THAay1NGo2HaKPrWMIE=
+
 "@types/babel__core@^7.1.0":
   version "7.1.4"
   resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.4.tgz#5c5569cc40e5f2737dfc00692f5444e871e4a234"