diff --git a/app/components/form.tsx b/app/components/form.tsx index 794f6e1..67ec726 100644 --- a/app/components/form.tsx +++ b/app/components/form.tsx @@ -13,7 +13,6 @@ import { getDateHistory, getMonthHistory, getOpenMeteoData, getWeekHistory } fro export default function Form(props: any) { const todaysDate = new Date() - const currentWeek = getWeekNumber(todaysDate) const [inputState, setInputState] = useState({ latitude: undefined, diff --git a/app/components/weatherPlot.tsx b/app/components/weatherPlot.tsx index c639816..1160f27 100644 --- a/app/components/weatherPlot.tsx +++ b/app/components/weatherPlot.tsx @@ -1,6 +1,6 @@ import { AreaSeries, Crosshair, DecorativeAxis, Highlight, HighlightArea, HorizontalGridLines, LineSeries, MarkSeries, VerticalBarSeries, XAxis, XYPlot, YAxis } from "react-vis"; import { formState, getWeekNumber, months, myColors, visualizationModes } from "../shared/utils"; -import { leastSquaresLinearRegression } from "../shared/mathHelpers"; +import { leastSquaresLinearRegression, theilSenEstimation } from "../shared/mathHelpers"; import { useState } from "react"; import styles from '../styles/form.module.css' diff --git a/app/shared/mathHelpers.ts b/app/shared/mathHelpers.ts index 3dc7e78..2b43cb9 100644 --- a/app/shared/mathHelpers.ts +++ b/app/shared/mathHelpers.ts @@ -1,8 +1,8 @@ -import { weatherPoint } from "./utils" +import { median, medianY, weatherPoint } from "./utils" // Least squares method to determine the line of best fit for the data export function leastSquaresLinearRegression(data: weatherPoint[]) { - + const xValues = Array.from(Array(data.length).keys()) const xMean = (xValues.reduce((a, b) => a + (b || 0), 0) / xValues.length) @@ -18,10 +18,42 @@ export function leastSquaresLinearRegression(data: weatherPoint[]) { // calculate xDiffs * yDiffs const xyDiffs = xDiffs.map((x, i) => x * yDiffs[i]) - // compute the steepness of the line and offset b + // compute the slope of the line and offset b const m = xyDiffs.reduce((a, b) => a + b, 0) / xDiffsSquared.reduce((a, b) => a + b, 0) const b = yMean - (xMean * m) const res = data.map((d, i) => ({ x: d.x, y: m * i + b })) return res +} + +// Theil Sen Estimation of best fitted line using median of all slopes through all point pairs +export function theilSenEstimation(data: weatherPoint[]) { + let slopes = [] + const xValues = Array.from(Array(data.length).keys()) + const yValues = data.map(d => Number(d.y)) + + for (var i = 0; i < data.length-1; i++) { + for (var j = i+1; j < data.length; j++) { + if (i !== j) { + // m = (y2 - y1)/(x2 - x1) + const m = (yValues[j] - yValues[i]) / (j-i) + slopes.push(m) + } + } + } + + // compute median + const medSlope = median(slopes) + + //b to be the median of the values yi − mxi. + let vals = [] + for (var i = 0; i < data.length; i++) { + const v = yValues[i] - (medSlope * i) + vals.push(v) + } + + const b = median(vals) + + const res = data.map((d, i) => ({ x: d.x, y: medSlope * i + b })) + return res } \ No newline at end of file diff --git a/app/shared/openMeteoInterface.tsx b/app/shared/openMeteoInterface.tsx index 85f4807..0f3ed33 100644 --- a/app/shared/openMeteoInterface.tsx +++ b/app/shared/openMeteoInterface.tsx @@ -1,4 +1,4 @@ -import { formState, getWeekNumber, inputState, median, months, myColors, visualizationModes } from "./utils"; +import { formState, getWeekNumber, inputState, median, medianY, months, myColors, visualizationModes } from "./utils"; import styles from '../styles/form.module.css' /** * @@ -62,7 +62,7 @@ export async function getOpenMeteoData(inputState: inputState, state: formState, setState({ ...state, tempData: [max, mean, min, prec], - tempDataMedian: mean.map((e: any) => { return { x: e.x, y: median(mean).y } }), + tempDataMedian: mean.map((e: any) => { return { x: e.x, y: medianY(mean).y } }), crosshairValues: [], keepCrosshair: false, currentVisMode: visualizationModes.Interval, @@ -118,7 +118,7 @@ export async function getDateHistory(inputState: inputState, state: formState, s setState({ ...state, tempData: [max, mean, min, prec], - tempDataMedian: mean.map((e: any) => { return { x: e.x, y: median(mean).y } }), + tempDataMedian: mean.map((e: any) => { return { x: e.x, y: medianY(mean).y } }), tempDataMean: mean.map((e: any) => { return { x: e.x, y: (avg || 0) } }), crosshairValues: [], keepCrosshair: false, @@ -210,7 +210,7 @@ export async function getWeekHistory(inputState: inputState, state: formState, s setState({ ...state, tempData: [max, mean, min, prec], - tempDataMedian: mean.map((e: any) => { return { x: e.x, y: median(mean).y } }), + tempDataMedian: mean.map((e: any) => { return { x: e.x, y: medianY(mean).y } }), crosshairValues: [], keepCrosshair: false, currentVisMode: visualizationModes.WeekHistory, @@ -284,17 +284,17 @@ export async function getMonthHistory(inputState: inputState, state: formState, // first filter out the nan values mean.forEach((d: any) => { d.y = d.y.filter((x: number) => !Number.isNaN((x))) - d.y = (d.y.reduce((a: number, b: any) => a + (b || 0), 0) / d.y.length).toFixed(2) + d.y = Number((d.y.reduce((a: number, b: any) => a + (b || 0), 0) / d.y.length).toFixed(2)) }); prec.forEach((d: any) => { - d.y = (d.y.reduce((a: number, b: any) => a + (b || 0), 0) /*/ d.y.length*/).toFixed(2) + d.y = Number((d.y.reduce((a: number, b: any) => a + (b || 0), 0) /*/ d.y.length*/).toFixed(2)) }); setState({ ...state, tempData: [max, mean, min, prec], - tempDataMedian: mean.map((e: any) => { return { x: e.x, y: median(mean).y } }), + tempDataMedian: mean.map((e: any) => { return { x: e.x, y: medianY(mean).y } }), crosshairValues: [], keepCrosshair: false, currentVisMode: visualizationModes.MonthHistory, diff --git a/app/shared/utils.tsx b/app/shared/utils.tsx index 54cb719..04496ad 100644 --- a/app/shared/utils.tsx +++ b/app/shared/utils.tsx @@ -97,11 +97,20 @@ export function getWeekNumber(d: Date) { return [d.getUTCFullYear(), res]; } -export const median = (arr: any) => { +export const medianY = (arr: weatherPoint[]) => { if (arr.length <= 2) { - return NaN; + return {x:null, y:NaN}; } - const mid = Math.floor(arr.length / 2), + const mid = Math.floor((arr.length) / 2), nums = [...arr].sort((a, b) => a.y - b.y); - return arr.length % 2 !== 0 ? nums[mid] : nums[mid + 1]; + return arr.length % 2 !== 0 ? nums[mid] : {x: null, y: (nums[mid-1].y+nums[mid].y)/2}; +}; + +export const median = (arr: number[]) => { + if (arr.length <= 2) { + return NaN; + } + const mid = Math.floor((arr.length) / 2), + nums = [...arr].sort((a, b) => a - b); + return arr.length % 2 !== 0 ? nums[mid] : (nums[mid-1]+nums[mid])/2; }; \ No newline at end of file