Package: ensemble-pkg
ensembleCombine models,easily.ensemblelets you combine your models and access them by a single object. You may use that ensemble to multiplex between your models, call them all, and aggregate the results. You can dobagging,boosting,stacking, and more. You may even create ensembles of ensembles!This package borrows the idea of computation graph sessioning fromTensorFlowand implements thecomposite patternfor building tree hierarchies.DocumentationRead the docs atensemble-pkg.readthedocs.ioInstallationpip install ensemble-pkgCase StudyLet's say we have two models that accomplish a binary classification task. We want to be able to easily combine the two models into an ensemble and then test the precision and recall of both models and the ensemble. With this package, it's easy.We start off by building our two models for a dataset. In this case, the task is classifying if a number is divisble by 15. We have two models, one says the number is divisble by 15 if it is divisble by 3 and the other says so if it is divisble by 5.defmodel1(x):returnx%3==0defmodel2(x):returnx%5==0defget_dataset():return[(i,i%15==0)foriinrange(1,101)]Define a function that gets our precision and recall for a given dataset and set of predictions:defget_results(dataset,preds):labels=[labelfor_,labelindataset]positives=sum(1forlabelinlabelsiflabel)predicted_positives=sum(1forpredinpredsifpred)true_positives=sum(1forlabel,predinzip(labels,preds)iflabelandpred)return100.0*true_positives/predicted_positives,100.0*true_positives/positivesNext we build a model ensemble frommodel1andmodel2, specifically one that only outputsTrueif all its children outputTrue. In this case, the ensemble would then only outputTrueif the input is both divisible by 3 and 5.e=Ensemble('ensemble',children=[model1,model2],mode='all')e(x=3)# returns Falsee(x=5)# returns Falsee(x=15)# returns TrueFinally, lets build another ensemble from our two models and the ensemble in order to easily aggregate the precision and recall stats for each model. We do this by decorating each child model with an evaluation decorator which modifies the models to take a dataset and output precision and recall, instead of taking a number and outputingTrueorFalse.defevaluate(model):defwrapper(dataset):preds=[model(x=x)forx,_indataset]precision,recall=get_results(dataset,preds)return{'precision':f'{precision:.1f}%','recall':f'{recall:.1f}%',}returnwrapperresults=Ensemble('results',children=[model1,model2,e])results.decorate_children(evaluate)Finally we runresults(dataset=get_dataset())and get the following results as expected!{'ensemble': {'precision': '100.0%', 'recall': '100.0%'},
 'model1': {'precision': '18.2%', 'recall': '100.0%'},
 'model2': {'precision': '30.0%', 'recall': '100.0%'}}In a few keystrokes, we built the following graph structure.ExamplesDefine your model functions and create your ensemble:>>>fromensembleimportEnsemble>>>defsquare(x):...returnx**2>>>defcube(x):...returnx**3>>>e=Ensemble(name='e1',children=[square,cube])Call all the models in the ensemble:>>>e(x=2){'square':4,'cube':8}>>>e(x=3){'square':9,'cube':27}Multiplex between functions:>>>e.multiplex('square',x=2)4>>>e.multiplex('cube',x=3)27You may instead decorate your model functions with@childin order to attach them to an ensemble:>>>fromensembleimportchild>>>@child('e2')...deffunc1(x):...returnx**2...>>>@child('e2')...deffunc2(x):...returnx**3...>>>e=Ensemble('e2')>>>e(x=3){'func1':9,'func2':27}You may even attach a model to multiple ensembles!>>>@child('e2','e3')...deffunc3(x,y):...returnx**3+y...>>>e2(x=2,y=3){'func1':4,'func2':8,'func3':11}>>>>>>e3=Ensemble('e3')>>>e3(x=2,y=3){'func3':11}Compute statstical aggregations from your ensemble's models:>>>defa(x):...returnx+1...>>>defb(y):...returny+2...>>>defc(z):...returnz+2...>>>e=Ensemble('e4',children=[a,b],weights=[3.0,1.0])>>>e.mean(x=2,y=3)4.0>>>e.weighted_mean(x=2,y=3)3.5>>>e.weighted_sum(x=2,y=3)14.0>>>e=Ensemble('e6',[a,b,c])>>>e.vote(x=1,y=1,z=1)3Build ensembles of ensembles!>>>first_ensemble=Ensemble('first',children=[c])>>>second_ensemble=Ensemble('second',children=[a,b])>>>parent_ensemble=Ensemble('parent',children=[first_ensemble,second_ensemble])>>>parent_ensemble(x=1,y=1,z=1){'first':{'c':3},'second':{'a':2,'b':3}}>>>parent_ensemble.multiplex('second',x=3,y=1){'a':4,'b':3}Use that idea to chain aggregate computations! Compute the mean of the sum of the model outputs in each ensemble:>>>first_ensemble.set_mode('sum')Ensemble(name='first',children=['c'],weights=None,mode='sum')>>>second_ensemble.set_mode('sum')Ensemble(name='second',children=['a','b'],weights=None,mode='sum')>>>parent_ensemble.mean(x=1,y=1,z=1)4.0If you forget what models are in your ensemble, just check:>>>print(parent_ensemble)Ensemble(name='parent',children=['first','second'],weights=None,mode='all')Ensemble(name='first',children=['c'],weights=None,mode='sum')Model(name='c',func=c(z))Ensemble(name='second',children=['a','b'],weights=None,mode='sum')Model(name='a',func=a(x))Model(name='b',func=b(y))In the above example, a tree is shown which shows which models and ensembles are the children of which ensembles!