Skip to content

Commit

Permalink
Theil Sen Estimation
Browse files Browse the repository at this point in the history
  • Loading branch information
lmueller27 committed Jul 11, 2023
1 parent a632e46 commit 2dbcf19
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 16 deletions.
1 change: 0 additions & 1 deletion app/components/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<inputState>({
latitude: undefined,
Expand Down
2 changes: 1 addition & 1 deletion app/components/weatherPlot.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand Down
38 changes: 35 additions & 3 deletions app/shared/mathHelpers.ts
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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
}
14 changes: 7 additions & 7 deletions app/shared/openMeteoInterface.tsx
Original file line number Diff line number Diff line change
@@ -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'
/**
*
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
17 changes: 13 additions & 4 deletions app/shared/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

0 comments on commit 2dbcf19

Please sign in to comment.