Beeminder state

An association containing:
- day: The current date.
- data: A TimeSeries of data entries, indexed by day.
- rates: A TimeSeries of rates, indexed by day.
Create a new instance with make[]. Optional parameters initialData and initialRate let you insert initial data on the initial day.
In[]:=
make[initialDay_,initialData_,initialRate_]:=<|​​"day"->initialDay,​​"data"->TimeSeries[{{initialDay,initialData}},MissingDataMethod{"Constant",0},ResamplingMethod{"Constant",0}],​​"rates"->TimeSeries[{{initialDay,initialRate}},MissingDataMethod{"Interpolation",InterpolationOrder0},ResamplingMethod{"Interpolation",InterpolationOrder0}]​​|>;​​make[day_]:=make[day,0,0];
Demo.
In[]:=
Datasetmake
Fri 17 Mar 2023

Out[]=
day
Fri 17 Mar 2023
data
TimeSeries
Time: 17 Mar 2023 to 17 Mar 2023
Data points: 1

rates
TimeSeries
Time: 17 Mar 2023 to 17 Mar 2023
Data points: 1


Derived Beeminder state

interpolatedData[] and interpolatedRates[] fill gaps with 0 (for data) or the most recent value (for rates). These plot better than the raw TimeSeries because plots don’t seem to respect the ResamplingMethod option, nor treat days with no data as Missing[].
In[]:=
interpolatedData[state_]:=TimeSeriesResample[state["data"],"Day"];​​interpolatedRates[state_]:=TimeSeriesResample[state["rates"],"Day"];
accumulatedData[] and accumulatedRates[] return TimeSeries corresponding to the lines you’d see in a Beeminder graph.
In[]:=
linearlyInterpolate[ts_,order_]:=TimeSeries[ts,MissingDataMethod{"Interpolation",InterpolationOrderorder},ResamplingMethod{"Interpolation",InterpolationOrderorder}]​​accumulatedData[state_]:=linearlyInterpolate[Accumulate[interpolatedData[state]],0];​​accumulatedRates[state_]:=linearlyInterpolate[Accumulate[interpolatedRates[state]],1];
clampedTimeSeriesAt[] returns 0 for days before the first data point and the last value for days after the last data point. This is algorithm what we want for all the TimeSeries in this file except accumulatedRates! TimeSeries interpolation works within the time range where there’s data, but it doesn’t extrapolate the way we want, so rely on this instead.
In[]:=
clampedTimeSeriesAt[ts_,day_]:=Block[{standardizeDate,sd},​​standardizeDate[date_]:=DateObject[DateValue[date,{"Year","Month","Day"}]];​​If[standardizeDate[day]<standardizeDate[ts["FirstDate"]],​​0,​​ts[Min[standardizeDate[day],standardizeDate[ts["LastDate"]]]]​​]​​];
rateAt[] returns the rate on any day.
In[]:=
rateAt[state_,day_]:=clampedTimeSeriesAt[interpolatedRates[state],day];
Demo.
In[]:=
Table​​Blockday=DatePlus
Fri 17 Mar 2023
,i,​​day,rateAtmake
Fri 17 Mar 2023
,0,.3,day,​​{i,-2,2}
Out[]=

Wed 15 Mar 2023
,0,
Thu 16 Mar 2023
,0,
Fri 17 Mar 2023
,0.3,
Sat 18 Mar 2023
,0.3,
Sun 19 Mar 2023
,0.3

Visualize

show[] draws the Beeminder graph (data and red line), the graph of entered data, and the graph of rate
In[]:=
show[state_]:=Column[{​​DateListPlot[{accumulatedData[state],accumulatedRates[state]}],​​BarChart[interpolatedData[state],PlotLabel"Data"],​​DateListPlot[interpolatedRates[state],PlotLabel"Rate"]​​}]

Beeminder state operations

update[] returns a new Beeminder state with some fields overwritten. change is an association or association list.
In[]:=
update[state_,change_]:=Merge[{state,change},Last];
advanceDate[] just moves the day forward by one. TODO: Auto-ratchet.
In[]:=
advanceDate[state_]:=update[state,"day"->DatePlus[state["day"],1]];
Demo.
In[]:=
advanceDatemake
Fri 17 Mar 2023
["day"]
Out[]=
Sat 18 Mar 2023
addDataOn[] and addData[] add one data point.
In[]:=
addDataOn[state_,day_,value_]:=update[state,"data"->TimeSeriesInsert[state["data"],{day,value}]];​​addData[state_,value_]:=addDataOn[state,state["day"],value];
Demo.
In[]:=
RightComposition[​​state|->addData[state,10],advanceDate,​​state|->addData[state,0],advanceDate,​​state|->addData[state,4],advanceDate​​]make
Fri 17 Mar 2023
//show
Out[]=
setRateOn[] sets the rate as of a particular date. TODO: If you set the rate on a day that already has a rate change, override it instead of adding!
In[]:=
setRateOn[state_,day_,value_]:=update[state,"rates"->TimeSeriesInsert[state["rates"],{day,value}]];
In[]:=
RightComposition[​​state|->setRateOn[state,state["day"],1],advanceDate,​​state|->setRateOn[state,state["day"],0],advanceDate,​​state|->setRateOn[state,state["day"],.5],advanceDate​​]make
Fri 17 Mar 2023
//show
Out[]=

Autodialing

Use the average data over the past 30 days to set the rate 8 days from now.
​
When less than 30 days of data is available, interpolate with the prior rate set for that day, using the fraction of the window that is available.
​
- strict: If True, then autodial will never lower the rate.
- multiplier: If autodial would have set the rate to rate, then set it to multiplier*rate instead.
- maxRate: If not Null, then this is the maximum rate autodial will set.
In[]:=
autodial[state_,strict_,multiplier_,maxRate_]:=Block[{windowDays=30,windowStart,realWindowDays,interp,window,rateDay,oldRate,newRate},​​(*Firstdayofthewindow*)​​windowStart=DatePlus[state["day"],-windowDays];​​​​(*Numberofdaysofdatathatwehavewithinthatwindow*)​​realWindowDays=Min[30,windowDays-QuantityMagnitude@DateDifference[windowStart,state["data"]["FirstDate"],"Day"]+1];​​​​(*Interpolationfactor:theratederivedfromthiswindowwillbeusedignoringthepreviousrateifafull30daysofdataisavailable.*)​​interp=realWindowDays/30;​​​​(*Datafromthelast30days*)​​window=TimeSeriesWindow[interpolatedData[state],​​{windowStart,state["day"]}];​​​​(*Thedayonwhichtosettherate:8daysfromnow*)​​rateDay=DatePlus[state["day"],8];​​​​(*Theratecurrentlysetforthatday*)​​oldRate=rateAt[state,rateDay];​​​​(*Calculatethenewrate,takingintoaccountalltheoptions*)​​newRate=Total[window]/realWindowDays;​​newRate=interp*newRate+(1-interp)*oldRate;​​newRate=multiplier*newRate;​​newRate=If[strict,Max[oldRate,newRate],newRate];​​newRate=If[maxRate===Null,newRate,Min[newRate,maxRate]];​​​​setRateOn[state,rateDay,newRate]​​];​​autodial[state_]:=autodial[state,False,1,Null];
Demo.

Simulation

sim[] simulates what will happen if you let autodialer do its thing…
​
- achieve: Function that returns how much data to log for a given day, given how much is due. The function’s input will be 0 on non-beemergency days.
- dial: Function intended for calling autodial. Its input and output are a Beeminder state.
- days: How many days to simulate.

What if I start with 7min/day, exercise 30min every beemergency, with multiplier set to 1.2?

Configurable sim