Package: segmented
segmentedsegmented is a Python toolbox for performing segmented regression, with an initial focus on characterizing parametric changepoints.  Thesegmented.democlass is for demonstration and replication purposes only (pleasesee herefor details).  Thesegmented.segmentedclass provides the core functionality.  This class currently permits connected, parametric segmented linear regression models with identity link functions.  Thesegmented.bayesclass permits Bayesian estimation of these same models. Eventually, the package will permit alternative link functions and other more flexible models to be specified.InstallationSegmented classBayes classDemo classInstallationsegmented is written for Python 3.7+.  The latest release of segmented can be installed from PyPI using pip:pip install segmentedSegmented ClassHere, we briefly outline the functionality of thesegmented.segmentedclass.  This class is intended to be the workhorse class of the segmented package.  This class currently permits the construction of segmented linear regression models that are a) connected, b) parametric (or non-parametric), and c) have identity link functions, and d) Gaussian noise.  Future releases will relax these requirements (see below).Let's see an example.  In what follows, we assume the following initial imports.  These are, coincidentally, (most of) the packages required by segmented:importpandasaspdimportnumpyasnpimportscipy.statsimportsegmentedassgmtLet's take a look at how the class works.  We'll start with a segmented regression model with anon-parametricchangepoint.x=np.array([0,1,2,3,4,5,6,7,8,9])y=np.array([0,0,0,0,0,0,1,2,3,4])data=pd.DataFrame({'x':x,'y':y})# construct a modelmodel=sgmt.segmented(['y~1+x','0+x'],data=data)# estimate parameter valuesmodel.fit([7])# check out resultprint(model.summary())Let's look at what we have here.  We first generate some data and then construct our model by callingsgmt.segmented().  We then estimate the parameters of our model by callingfit()and finally we inspect the results by callingsummary().  Let's take a closer look at the model specification step.The first argument tosgmt.segmented()is a list ofpatsystrings, each providing a specification for a separate segment of our model.  This listof segment specifications is always the first argument.  The second, named argument is the data we are modeling and must come in the form of apandas dataframe.  In this example we have provided 2-segment specification.Every segment in our segmented regression model,including the first, consists of what we refer to as anodeat the beginning (left edge) of the segment and a line that spans the interval between that segment's node (i.e., to the left) and the node associated with the next segment (i.e., to the right).  The first segment begins at with a node at x=min(x) that we will call x=T1.  The second segment begins with a node that marks the point at which the two segments are connected.  We refer to this point as x=T2.The first segment specification provides 2 important pieces of information about our model.  First, it instructssegmentedto treatdata['y']as the outcome variable.  Second, it indicates that an intercept-like term will be estimated (cf.'y~0+x').  Specifically, we estimate an offset, B0, such that y = f(T1) = f(min(x)) = B0.The second item in the specification list describes the second segment and is a bit different.  There are two details in particular that are worth highlighting.First, we explicitly omit an intercept by including the0in'~0+x'.  This means thatsegmented()will constructconnectedsegments.  If we had instead included an intercept in these segment definitions (e.g.,'~1+x'or, more implicitly,'~x'), we would instead permit the model to construct adisconnectedmodel in which the segments need not meet at each node (not yet implemented).Second, we have omitted the outcome variable (i.e., y) from the left-hand side of the second segment's specification. This is because, unlike the first segment's specification, the second segment's specificationdoes notdescribe the relationship between the outcome variable and the predictor (at least not in a straightforward sense).  Instead, the second segment's specification describes how we wish thedifferencebetween the first segment and the second segment to be modeled.  The segment specification we provided suggests that the change occurring at the that segment's node (i.e., to the left) can be described by a simple change in (linear) slope (as a function ofdata['x']).  Thus, for each segment, there will be a single slope (coefficient) estimated for each segment: B1 for the first segment and B2 for the second segment.  The first segment will work a bit like conventional linear regression: y = B0 + B1 (x - T1).  However, within the second segment, y = B0 + B1 (x - T1) + B2 (x - T2)+.  Here, the (x - T2)+ = x - T2 if x > T2 and 0 otherwise.We then callfit()and pass it a list of guesses about where the changepoints might be.  Because we have specified a model with 2 segements, we provide a list consisting of a guess aboute the single changepoint (i.e.,[7]).Finally, we inspect the results by callingsummary().  This will provide a variety of details regarding the optimization procedure.Now let's take a look at a segmented regression model with aparametricchangepoint.# generate some datax=np.array([0,1,2,3,4,5,6,7,8,9])b=np.array([0,1,-1])cp=[2,0]y=b[0]+(b[1]*x)y+=(b[2]*np.clip(x-cp[0],0,None))z=cp[1]*np.ones_like(x)# not useddata=pd.DataFrame({'x':x,'y':y,'z':z})# construct a modelmodel=sgmt.segmented(['y~1+x','0+x'],changepoints=['1+z'],data=data)# estimate parameter valuesmodel.fit([0,0,0,data['x'].median(),0,1])# check out resultprint(model.summary())This example is very similar to the non-parametric example.  We generate some data and then construct our model by passing a list of specifications and our data.  However, we also pass in a list of changepoint specifications.  Because we specified a model with 2 segments, we supply a single changepoint specification.  Here, we have specified that the changepoint will be a linear function of the variable z.  We have included an intercept term (the'1'in'1+z').  So the changepoint, T2, that sits "in between" the first and second segments will be located at T2 = G0 + G1*z.  When we callfit()we supply a list of preliminary parameter values.  There are 6 values in this list: B0, B1, B2, G0, G1, and the log of the standard deviation of the (normally distributed) error term.Current limits (to be relaxed in future releases):Two segments maximumSegmented currently only permits 2 segments (1 changepoint).  This will be relaxed in future releases, first for non-parametric changepoint model (already implemented) and subsequently for parametric changepoint models (where things can get a bit more complicated).Identity link functionsSegmented currently only permits identity link functions with normally-distributed error structures.  Additional link functions (e.g., log-linear) and error functions (e.g., Student's t) will added in future releases.One predictor maximumSegmented currently only permits a single predictor variable in the segment specifications (e.g., x).  We plan to relax this constraint in a future release, but the API may need to be reivsed to allow users to indicate which, of many, predictor is the primary predictor (i.e., the dimension along which the changepoints are defined).Connected models onlySegmented currently only permits connected models.  This means that the second segment's specificationmay notinclude an intercept-like term.  That is, this is valid:# validmodel=sgmt.segmented(['y~1+x','0+x'],changepoints=['1+z'],data=data)This isnotvalid:# not validmodel=sgmt.segmented(['y~1+x','1+x'],changepoints=['1+z'],data=data)Disconnected models will be permitted in a future release.Bayes ClassThis class is a new addition to segmented and, as the name implies, takes a Bayesian approach to the segmented regression modeldescribed above.  The Bayesian approach has a variety of advantages (e.g., increased flexibility for developers, far more information for users).  The  interface to the Bayesian class is similar to the segmented class described above.  The feature set is not quite equivalent, but is advancing rapidly.  Currently the Bayes class permits:Unlimited number of segmentsUnlimited number of predictor variablesBoth non-parameteric and parametric changepointsBoth connectedand disconnectedsegmentsBut the Bayes class is still limited to :Identity link functionsThese features make the Bayes class quite a bit more sophisticated than the segmented class.  For this reason, the Bayes class will replace segmented as the primary focus of the package.Here is an example of the Bayes class in use:importarvizasazimportnumpyasnpimportpandasaspdrng=rng=np.random.default_rng(12345)# generate some datadata=pd.DataFrame({'y':rng.random(size=100),'x':rng.random(size=100)})# build a modelmodel=sgmt.bayes(['y~1+x']+2*['0+x'],changepoints=2*['1'],data=data)# fit the modelidata=model.fit()# summarize the posterior samplesaz.summary(trace)Please be aware that the Bayes class requires a couple of additional packages to function.  It requiresemceefor sampling andarvizfor processing of the posterior samples.Demo ClassThis class embodies a segmented regression model with parametric node placement.  The specification of both the segmented regression model itself and the specification of the parametric node placement are log-linear with Poisson error structures.  Thesegmented.democlass also assumes a single node (i.e., two segments).Let's generate some synthetic data to use for estimation.# number of datapoints in our data setn_samples=100# sample x and z uniformly [0,100]rng=np.random.default_rng()x=100*rng.random(size=n_samples)z=100*rng.random(size=n_samples)data=pd.DataFrame({'x':x,'z':z})# define parametersb=[3.5,-.015,.025]g=[np.log(25),.011]# generate ynode_mean=np.exp(g[0]+(g[1]*data['z']))nodes=scipy.stats.poisson.rvs(node_mean)y_mean=np.exp(b[0]+(b[1]*data['x'])+((data['x']>nodes)*(b[2]*(data['x']-nodes))))data['y']=scipy.stats.poisson.rvs(y_mean)Here, we have generated data reflecting 2 segments separated by a single node.  This node is parametric: its location is dependent ondata['z'].  Now that we have our data, let's construct and fit our model.# construct modelmodel=sgmt.demo(data=data)# fit parametric node placement segmented regression modelmodel.fit(bounds=[(-5,5),(-.05,.05),(-.05,.05),(-5,5),(-.05,.05)])Here, we initialize the demonstration model by passing in our data to the initialization.  We then callfit()to begin the estimation/optimization process, providing bounds to constrain the parameter values considered.  Once this function call returns, we can unpack our results and see what happened.# print summary of optimizationprint(model.summary())# compare parameter estimates to true valuesfori,jinzip(b+g,model.result.x):print(f'{i:0.4f}\t-\t{j:0.4f}')Currently,summary()returns ascipy.optimize.OptimizeResultobject.