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
|
||||
Traps []TrapNearby
|
||||
Treatments []Treatment
|
||||
TreatmentCadence time.Duration
|
||||
//TreatmentCadence TreatmentCadence
|
||||
TreatmentModels []TreatmentModel
|
||||
User User
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
treatment_times := make([]time.Time, 0)
|
||||
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
|
||||
}
|
||||
treatment_models := modelTreatment(treatments)
|
||||
data := ContentSource{
|
||||
Inspections: inspections,
|
||||
MapData: ComponentMap{
|
||||
|
|
@ -534,7 +524,7 @@ func htmlSource(w http.ResponseWriter, r *http.Request, user *models.User, id st
|
|||
Source: s,
|
||||
Traps: traps,
|
||||
Treatments: treatments,
|
||||
TreatmentCadence: cadence,
|
||||
TreatmentModels: treatment_models,
|
||||
User: userContent,
|
||||
}
|
||||
|
||||
|
|
@ -577,6 +567,7 @@ func makeFuncMap() template.FuncMap {
|
|||
"bigNumber": bigNumber,
|
||||
"GISStatement": gisStatement,
|
||||
"latLngDisplay": latLngDisplay,
|
||||
"timeAsRelativeDate": timeAsRelativeDate,
|
||||
"timeDelta": timeDelta,
|
||||
"timeElapsed": timeElapsed,
|
||||
"timeInterval": timeInterval,
|
||||
|
|
@ -642,6 +633,10 @@ func parseFromDisk(files []string) (*template.Template, error) {
|
|||
return templ, nil
|
||||
}
|
||||
|
||||
func timeAsRelativeDate(d time.Time) string {
|
||||
return d.Format("01-02")
|
||||
}
|
||||
|
||||
// FormatTimeDuration returns a human-readable string representing a time.Duration
|
||||
// as "X units early" or "X units late"
|
||||
func timeDelta(d time.Duration) string {
|
||||
|
|
|
|||
|
|
@ -250,7 +250,24 @@
|
|||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<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">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -316,7 +333,7 @@
|
|||
<tbody>
|
||||
<tr>
|
||||
<td class="info-label">Trap ID:</td>
|
||||
<td>{{ .ID }}</td>
|
||||
<td><a href="/trap/{{.ID}}">{{ .ID }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="info-label">Distance</td>
|
||||
|
|
|
|||
189
time.go
189
time.go
|
|
@ -1,42 +1,161 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
//"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// calculateCadenceVariance takes a slice of time.Time instances and returns
|
||||
// the average time span between consecutive instances and how much each
|
||||
// instance deviates from its expected position in an evenly-spaced sequence
|
||||
// timestamps should be in descending order (most recent time in index 0)
|
||||
func calculateCadenceVariance(timestamps []time.Time) (averageSpan time.Duration, deviations []time.Duration) {
|
||||
if len(timestamps) <= 1 {
|
||||
return 0, []time.Duration{}
|
||||
}
|
||||
|
||||
// Calculate total span between first and last timestamp
|
||||
totalSpan := timestamps[0].Sub(timestamps[len(timestamps)-1])
|
||||
|
||||
// Calculate average span
|
||||
averageSpan = totalSpan / time.Duration(len(timestamps)-1)
|
||||
if averageSpan < 0 {
|
||||
log.Error().Int64("average", int64(averageSpan)).Msg("Negative average")
|
||||
return 0, []time.Duration{}
|
||||
}
|
||||
|
||||
// Calculate deviations
|
||||
deviations = make([]time.Duration, len(timestamps))
|
||||
|
||||
// The first timestamp is our reference point (deviation = 0)
|
||||
deviations[0] = 0
|
||||
|
||||
// Calculate expected timestamps based on perfect intervals
|
||||
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")
|
||||
return averageSpan, deviations
|
||||
// TreatmentModel represents the calculated model for a year's treatments
|
||||
type TreatmentModel struct {
|
||||
Year int
|
||||
SeasonStart time.Time
|
||||
SeasonEnd time.Time
|
||||
Interval time.Duration
|
||||
ActualDates []time.Time
|
||||
PredictedDates []time.Time
|
||||
Errors []time.Duration
|
||||
}
|
||||
|
||||
func modelTreatment(treatments []Treatment) []TreatmentModel {
|
||||
treatment_times := make([]time.Time, 0)
|
||||
for _, treatment := range treatments {
|
||||
treatment_times = append(treatment_times, treatment.Date)
|
||||
}
|
||||
models := calculateTreatmentModels(treatment_times)
|
||||
/*models_by_year := make(map[int]TreatmentModel)
|
||||
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
|
||||
}
|
||||
|
||||
// calculateTreatmentModels segments treatments by year and calculates a model for each year
|
||||
func calculateTreatmentModels(treatments []time.Time) []TreatmentModel {
|
||||
// Group treatments by year
|
||||
yearMap := make(map[int][]time.Time)
|
||||
for _, t := range treatments {
|
||||
year := t.Year()
|
||||
yearMap[year] = append(yearMap[year], t)
|
||||
}
|
||||
|
||||
// Calculate a model for each year
|
||||
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