Beeminder state
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.
- 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",InterpolationOrder0},ResamplingMethod{"Interpolation",InterpolationOrder0}]|>;make[day_]:=make[day,0,0];
Demo.
In[]:=
Datasetmake
Out[]=
Derived Beeminder state
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",InterpolationOrderorder},ResamplingMethod{"Interpolation",InterpolationOrderorder}]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[]:=
TableBlockday=DatePlus,i,day,rateAtmake,0,.3,day,{i,-2,2}
Out[]=
,0,,0,,0.3,,0.3,,0.3
Visualize
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
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[]:=
advanceDatemake["day"]
Out[]=
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//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//show
Out[]=
Autodialing
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.
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
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.
- 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?
What if I start with 7min/day, exercise 30min every beemergency, with multiplier set to 1.2?
Configurable sim
Configurable sim