func (p *Pipeline) GetNOutputs() int {
	if len(p.NamedSteps) == 0 {
		return 0
	}
	return p.NamedSteps[len(p.NamedSteps)-1].Fiter.(base.Predicter).GetNOutputs()
}
GetNOutputs returns output columns number for Y
package pipeline
import (
	"fmt"
	"strings"
	"github.com/pa-m/sklearn/base"
	"github.com/pa-m/sklearn/preprocessing"
	"gonum.org/v1/gonum/mat"
)
type NamedStep struct {
	Name string
	base.Fiter
}
type Pipeline struct {
	NamedSteps []NamedStep
	NOutputs int
}
func NewPipeline(steps ...NamedStep) *Pipeline {
	p := &Pipeline{NamedSteps: steps}
	return p
}
func (p *Pipeline) PredicterClone() base.Predicter {
	clone := *p
	clone.NamedSteps = make([]NamedStep, len(p.NamedSteps))
	for i, step := range p.NamedSteps {
		if cloner, ok := step.Fiter.(base.Transformer); ok {
			clone.NamedSteps[i] = NamedStep{Name: step.Name, Fiter: cloner.TransformerClone()}
		} else if cloner, ok := step.Fiter.(base.Predicter); ok {
			clone.NamedSteps[i] = NamedStep{Name: step.Name, Fiter: cloner.PredicterClone()}
		} else {
			panic(fmt.Errorf("step %s is not clonable", step.Name))
		}
	}
	return &clone
}
func (p *Pipeline) IsClassifier() bool {
	nSteps := len(p.NamedSteps)
	if nSteps > 0 {
		if clf, ok := p.NamedSteps[nSteps-1].Fiter.(base.Predicter); ok {
			return clf.IsClassifier()
		}
	}
	return false
}
func (p *Pipeline) transformStep(istep int, Xtmp, Ytmp **mat.Dense) {
	step := p.NamedSteps[istep]
	if transformer, ok := step.Fiter.(base.Transformer); ok {
		*Xtmp, *Ytmp = transformer.Transform(*Xtmp, *Ytmp)
	} else if predicter, ok := step.Fiter.(base.Predicter); ok {
		nOutputs := predicter.GetNOutputs()
		if nOutputs == 0 {
			panic(fmt.Errorf("pipeline step %d (%s) predicter with no output", istep, step.Name))
		}
		nSamples, _ := (*Xtmp).Dims()
		*Ytmp = mat.NewDense(nSamples, nOutputs, nil)
		predicter.Predict(*Xtmp, *Ytmp)
	} else {
		if istep < len(p.NamedSteps)-1 {
			panic(fmt.Errorf("pipeline step %d (%s) is not a Transformer", istep, step.Name))
		} else {
			panic(fmt.Errorf("pipeline step %d (%s) is not a Predicter", istep, step.Name))
		}
	}
}
func (p *Pipeline) Fit(Xmatrix, Ymatrix mat.Matrix) base.Fiter {
	X, Y := base.ToDense(Xmatrix), base.ToDense(Ymatrix)
	_, p.NOutputs = Y.Dims()
	Xtmp, Ytmp := X, Y
	steps := len(p.NamedSteps)
	for istep, step := range p.NamedSteps {
		step.Fit(Xtmp, Ytmp)
		if istep < steps-1 {
			p.transformStep(istep, &Xtmp, &Ytmp)
		}
	}
	return p
}
func (p *Pipeline) Score(X, Y mat.Matrix) float64 {
	Xtmp, Ytmp := base.ToDense(X), base.ToDense(Y)
	for istep := range p.NamedSteps[:len(p.NamedSteps)-1] {
		p.transformStep(istep, &Xtmp, &Ytmp)
	}
	last := p.NamedSteps[len(p.NamedSteps)-1]
	predicter, ok := last.Fiter.(base.Predicter)
	if !ok {
		panic(fmt.Errorf("pipeline.Score: last step is not a Predicter"))
	}
	return predicter.Score(Xtmp, Y)
}