Calculate treatment cadence by year
This commit is contained in:
parent
689cc0319d
commit
009dc29e5d
3 changed files with 200 additions and 69 deletions
23
html.go
23
html.go
|
|
@ -128,7 +128,8 @@ type ContentSource struct {
|
||||||
Source *BreedingSourceDetail
|
Source *BreedingSourceDetail
|
||||||
Traps []TrapNearby
|
Traps []TrapNearby
|
||||||
Treatments []Treatment
|
Treatments []Treatment
|
||||||
TreatmentCadence time.Duration
|
//TreatmentCadence TreatmentCadence
|
||||||
|
TreatmentModels []TreatmentModel
|
||||||
User User
|
User User
|
||||||
}
|
}
|
||||||
type Inspection struct {
|
type Inspection struct {
|
||||||
|
|
@ -506,18 +507,7 @@ func htmlSource(w http.ResponseWriter, r *http.Request, user *models.User, id st
|
||||||
respondError(w, "Failed to get treatments", err, http.StatusInternalServerError)
|
respondError(w, "Failed to get treatments", err, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
treatment_times := make([]time.Time, 0)
|
treatment_models := modelTreatment(treatments)
|
||||||
for _, treatment := range treatments {
|
|
||||||
treatment_times = append(treatment_times, treatment.Date)
|
|
||||||
}
|
|
||||||
cadence, deltas := calculateCadenceVariance(treatment_times)
|
|
||||||
for i, treatment := range treatments {
|
|
||||||
if i >= len(deltas) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
treatment.CadenceDelta = deltas[i]
|
|
||||||
treatments[i] = treatment
|
|
||||||
}
|
|
||||||
data := ContentSource{
|
data := ContentSource{
|
||||||
Inspections: inspections,
|
Inspections: inspections,
|
||||||
MapData: ComponentMap{
|
MapData: ComponentMap{
|
||||||
|
|
@ -534,7 +524,7 @@ func htmlSource(w http.ResponseWriter, r *http.Request, user *models.User, id st
|
||||||
Source: s,
|
Source: s,
|
||||||
Traps: traps,
|
Traps: traps,
|
||||||
Treatments: treatments,
|
Treatments: treatments,
|
||||||
TreatmentCadence: cadence,
|
TreatmentModels: treatment_models,
|
||||||
User: userContent,
|
User: userContent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -577,6 +567,7 @@ func makeFuncMap() template.FuncMap {
|
||||||
"bigNumber": bigNumber,
|
"bigNumber": bigNumber,
|
||||||
"GISStatement": gisStatement,
|
"GISStatement": gisStatement,
|
||||||
"latLngDisplay": latLngDisplay,
|
"latLngDisplay": latLngDisplay,
|
||||||
|
"timeAsRelativeDate": timeAsRelativeDate,
|
||||||
"timeDelta": timeDelta,
|
"timeDelta": timeDelta,
|
||||||
"timeElapsed": timeElapsed,
|
"timeElapsed": timeElapsed,
|
||||||
"timeInterval": timeInterval,
|
"timeInterval": timeInterval,
|
||||||
|
|
@ -642,6 +633,10 @@ func parseFromDisk(files []string) (*template.Template, error) {
|
||||||
return templ, nil
|
return templ, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func timeAsRelativeDate(d time.Time) string {
|
||||||
|
return d.Format("01-02")
|
||||||
|
}
|
||||||
|
|
||||||
// FormatTimeDuration returns a human-readable string representing a time.Duration
|
// FormatTimeDuration returns a human-readable string representing a time.Duration
|
||||||
// as "X units early" or "X units late"
|
// as "X units early" or "X units late"
|
||||||
func timeDelta(d time.Duration) string {
|
func timeDelta(d time.Duration) string {
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,24 @@
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<p>Estimated Treatment Cadence: {{.TreatmentCadence|timeInterval}}</p>
|
<table class="table table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Year</th>
|
||||||
|
<th>Start</th>
|
||||||
|
<th>End</th>
|
||||||
|
<th>Interval</th>
|
||||||
|
</tr>
|
||||||
|
{{ range .TreatmentModels }}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Year}}</td>
|
||||||
|
<td>{{.SeasonStart|timeAsRelativeDate}}</td>
|
||||||
|
<td>{{.SeasonEnd|timeAsRelativeDate}}</td>
|
||||||
|
<td>{{.Interval|timeInterval}}</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -316,7 +333,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="info-label">Trap ID:</td>
|
<td class="info-label">Trap ID:</td>
|
||||||
<td>{{ .ID }}</td>
|
<td><a href="/trap/{{.ID}}">{{ .ID }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="info-label">Distance</td>
|
<td class="info-label">Distance</td>
|
||||||
|
|
|
||||||
177
time.go
177
time.go
|
|
@ -1,42 +1,161 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
//"github.com/rs/zerolog/log"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// calculateCadenceVariance takes a slice of time.Time instances and returns
|
// TreatmentModel represents the calculated model for a year's treatments
|
||||||
// the average time span between consecutive instances and how much each
|
type TreatmentModel struct {
|
||||||
// instance deviates from its expected position in an evenly-spaced sequence
|
Year int
|
||||||
// timestamps should be in descending order (most recent time in index 0)
|
SeasonStart time.Time
|
||||||
func calculateCadenceVariance(timestamps []time.Time) (averageSpan time.Duration, deviations []time.Duration) {
|
SeasonEnd time.Time
|
||||||
if len(timestamps) <= 1 {
|
Interval time.Duration
|
||||||
return 0, []time.Duration{}
|
ActualDates []time.Time
|
||||||
|
PredictedDates []time.Time
|
||||||
|
Errors []time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate total span between first and last timestamp
|
func modelTreatment(treatments []Treatment) []TreatmentModel {
|
||||||
totalSpan := timestamps[0].Sub(timestamps[len(timestamps)-1])
|
treatment_times := make([]time.Time, 0)
|
||||||
|
for _, treatment := range treatments {
|
||||||
// Calculate average span
|
treatment_times = append(treatment_times, treatment.Date)
|
||||||
averageSpan = totalSpan / time.Duration(len(timestamps)-1)
|
}
|
||||||
if averageSpan < 0 {
|
models := calculateTreatmentModels(treatment_times)
|
||||||
log.Error().Int64("average", int64(averageSpan)).Msg("Negative average")
|
/*models_by_year := make(map[int]TreatmentModel)
|
||||||
return 0, []time.Duration{}
|
for _, m := range models {
|
||||||
|
models_by_year[m.Year] = m
|
||||||
|
}*/
|
||||||
|
offset := 0
|
||||||
|
for _, model := range models {
|
||||||
|
for _, e := range model.Errors {
|
||||||
|
treatments[offset].CadenceDelta = e
|
||||||
|
offset = offset + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
for i, treatment := range treatments {
|
||||||
|
model
|
||||||
|
treatment.CadenceDelta = deltas[i]
|
||||||
|
treatments[i] = treatment
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
/*cadence, deltas := calculateCadenceVariance(treatment_times)
|
||||||
|
for i, treatment := range treatments {
|
||||||
|
if i >= len(deltas) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
treatment.CadenceDelta = deltas[i]
|
||||||
|
treatments[i] = treatment
|
||||||
|
}*/
|
||||||
|
return models
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate deviations
|
// calculateTreatmentModels segments treatments by year and calculates a model for each year
|
||||||
deviations = make([]time.Duration, len(timestamps))
|
func calculateTreatmentModels(treatments []time.Time) []TreatmentModel {
|
||||||
|
// Group treatments by year
|
||||||
// The first timestamp is our reference point (deviation = 0)
|
yearMap := make(map[int][]time.Time)
|
||||||
deviations[0] = 0
|
for _, t := range treatments {
|
||||||
|
year := t.Year()
|
||||||
// Calculate expected timestamps based on perfect intervals
|
yearMap[year] = append(yearMap[year], t)
|
||||||
for i := 1; i < len(timestamps); i++ {
|
|
||||||
expectedTime := timestamps[0].Add(-averageSpan * time.Duration(i))
|
|
||||||
deviations[i] = timestamps[i].Sub(expectedTime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Int64("average", int64(averageSpan)).Int("number deviations", len(deviations)).Msg("Calculated cadence")
|
// Calculate a model for each year
|
||||||
return averageSpan, deviations
|
var models []TreatmentModel
|
||||||
|
for year, dates := range yearMap {
|
||||||
|
// Sort dates within the year
|
||||||
|
sort.Slice(dates, func(i, j int) bool {
|
||||||
|
return dates[i].Before(dates[j])
|
||||||
|
})
|
||||||
|
|
||||||
|
model := calculateYearModel(year, dates)
|
||||||
|
models = append(models, model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort models by year
|
||||||
|
sort.Slice(models, func(i, j int) bool {
|
||||||
|
return models[i].Year < models[j].Year
|
||||||
|
})
|
||||||
|
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateYearModel creates a model for a specific year using linear regression
|
||||||
|
func calculateYearModel(year int, dates []time.Time) TreatmentModel {
|
||||||
|
n := len(dates)
|
||||||
|
if n < 2 {
|
||||||
|
// Not enough data for a model
|
||||||
|
return TreatmentModel{
|
||||||
|
Year: year,
|
||||||
|
ActualDates: dates,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert dates to numeric values (seconds since epoch)
|
||||||
|
var x []float64
|
||||||
|
var y []float64
|
||||||
|
for i, date := range dates {
|
||||||
|
x = append(x, float64(i))
|
||||||
|
y = append(y, float64(date.Unix()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate linear regression
|
||||||
|
slope, intercept := linearRegression(x, y)
|
||||||
|
|
||||||
|
// Convert back to time.Time and time.Duration
|
||||||
|
startTime := time.Unix(int64(intercept), 0)
|
||||||
|
intervalSeconds := int64(slope)
|
||||||
|
interval := time.Duration(intervalSeconds) * time.Second
|
||||||
|
|
||||||
|
// Calculate end of season
|
||||||
|
endTime := time.Unix(int64(intercept+slope*float64(n-1)), 0)
|
||||||
|
|
||||||
|
// Generate predicted dates and calculate errors
|
||||||
|
var predictedDates []time.Time
|
||||||
|
var errors []time.Duration
|
||||||
|
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
predicted := time.Unix(int64(intercept+slope*float64(i)), 0)
|
||||||
|
predictedDates = append(predictedDates, predicted)
|
||||||
|
|
||||||
|
// Calculate error
|
||||||
|
actualTime := dates[i]
|
||||||
|
error := actualTime.Sub(predicted)
|
||||||
|
errors = append(errors, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return TreatmentModel{
|
||||||
|
Year: year,
|
||||||
|
SeasonStart: startTime,
|
||||||
|
SeasonEnd: endTime,
|
||||||
|
Interval: interval,
|
||||||
|
ActualDates: dates,
|
||||||
|
PredictedDates: predictedDates,
|
||||||
|
Errors: errors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// linearRegression calculates the slope and intercept of the best-fit line
|
||||||
|
func linearRegression(x, y []float64) (float64, float64) {
|
||||||
|
n := float64(len(x))
|
||||||
|
if n < 2 {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var sumX, sumY, sumXY, sumX2 float64
|
||||||
|
for i := 0; i < len(x); i++ {
|
||||||
|
sumX += x[i]
|
||||||
|
sumY += y[i]
|
||||||
|
sumXY += x[i] * y[i]
|
||||||
|
sumX2 += x[i] * x[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate slope
|
||||||
|
slope := (n*sumXY - sumX*sumY) / (n*sumX2 - sumX*sumX)
|
||||||
|
|
||||||
|
// Calculate intercept
|
||||||
|
intercept := (sumY - slope*sumX) / n
|
||||||
|
|
||||||
|
return slope, intercept
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue