pascalabcnet/bin/Lib/MLPipelineABC.pas
Mikhalkovich Stanislav 10c929fa1c Правки ML
2026-04-19 21:07:00 +03:00

1155 lines
36 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

unit MLPipelineABC;
interface
uses MLCoreABC;
uses PreprocessorABC;
uses DataFrameABC;
uses LinearAlgebraML;
type
TaskKind = (tkRegression, tkClassification);
type
PipelineBase = abstract class
protected
fDataSteps: List<IPreprocessor>;
fMatrixSteps: List<ITransformer>;
fFeatures: array of string;
fFinalFeatures: array of string;
fFitted: boolean;
procedure ValidateSchema(df: DataFrame); virtual; abstract;
function PrepareMatrix(df: DataFrame; var current: DataFrame): Matrix;
function FitTransformMatrix(var X: Matrix; y: Vector := nil): Matrix;
function TransformMatrix(X: Matrix): Matrix;
public
constructor Create;
/// Признак того, что был вызван Fit или FitTransform.
property IsFitted: boolean read fFitted;
end;
/// DataPipeline — конвейер подготовки данных и обучения модели с учителем на DataFrame.
///
/// Поддерживает два уровня шагов:
/// • DataFrame-уровень: IPreprocessor (Fit/Transform над DataFrame)
/// • Matrix-уровень: ITransformer / ISupervisedTransformer и IModel
/// (после преобразования DataFrame → Matrix/Vector)
///
/// Правила порядка шагов:
/// • Сначала идут только DataFrame-шаги (IPreprocessor).
/// • Затем — матричные шаги (ITransformer / ISupervisedTransformer).
/// • Модель (IModel) добавляется последней и может быть только одна.
///
/// В режиме с моделью используются:
/// • features — признаки;
/// • target — целевая переменная.
///
/// Если модель не добавлена, DataPipeline работает как чистый DF-конвейер (Fit/Transform/FitTransform)
DataPipeline = class(PipelineBase, IModel)
private
fModel: ISupervisedModel;
fTask: TaskKind;
fTarget: string;
protected
procedure ValidateSchema(df: DataFrame); override;
public
/// Создаёт пустой конвейер
constructor Create;
/// Добавляет шаг в конец конвейера.
/// Принимает:
/// • IPreprocessor (DataFrame-уровень)
/// • ITransformer (Matrix-уровень)
/// • ISupervisedModel (модель, должна быть последней)
/// Запрещено добавлять шаги после вызова Fit/FitTransform.
function Add(step: IPipelineStep): DataPipeline;
/// Строит конвейер из шагов обработки данных и модели.
///
/// Используется в задачах с учителем (с target).
/// task задает тип модели - регрессия или классификация
/// Шаги выполняются последовательно:
/// DataFrame-преобразования → (при необходимости) матричные шаги → модель.
static function Build(
task: TaskKind;
target: string;
features: array of string;
params steps: array of IPipelineStep
): DataPipeline;
/// Обучает конвейер на DataFrame.
/// Семантика:
/// • выполняет Fit/Transform для всех DataFrame-шагов;
/// • если присутствует модель — преобразует DataFrame в (X, y) по features/target,
/// затем обучает матричные трансформеры и модель.
function Fit(df: DataFrame): DataPipeline;
/// Применяет обученные DataFrame-шаги к DataFrame и возвращает новый DataFrame.
/// В ML-режиме Transform не выполняет матричные шаги и не вызывает модель.
function Transform(df: DataFrame): DataFrame;
/// Выполняет Fit и Transform за один проход для DataFrame-режима.
/// Недоступен, если конвейер содержит модель (IModel).
function FitTransform(df: DataFrame): DataFrame;
/// Делает предсказание модели для объектов из DataFrame.
/// Доступен только если конвейер содержит модель (IModel) и был обучен (Fit).
function Predict(df: DataFrame): Vector;
/// Возвращает матрицу вероятностей (nSamples × nClasses).
/// Доступен только если конечная модель поддерживает IProbabilisticClassifier.
function PredictProba(df: DataFrame): Matrix;
/// Возвращает метки классов в порядке кодирования (0,1,2,...),
/// используемом при EncodeLabels.
/// Доступен только для задач классификации после Fit.
function GetClassLabels: array of string;
function ToString: string; override;
function Name: string := Self.GetType.Name;
function Clone: IModel;
end;
/// UDataPipeline — конвейер подготовки данных и обучения модели без учителя на DataFrame.
///
/// Поддерживает два уровня шагов:
/// • DataFrame-уровень: IPreprocessor (Fit/Transform над DataFrame)
/// • Matrix-уровень: IUnsupervisedTransformer и IUnsupervisedModel
/// (после преобразования DataFrame → Matrix)
///
/// Правила порядка шагов:
/// • Сначала идут только DataFrame-шаги (IPreprocessor).
/// • Затем — матричные шаги (IUnsupervisedTransformer).
/// • Модель (IUnsupervisedModel) добавляется последней и может быть только одна.
///
/// В конвейере используются:
/// • features — признаки (целевая переменная отсутствует).
///
/// Если модель не добавлена, UDataPipeline работает как чистый DF-конвейер
/// (Fit/Transform/FitTransform)
UDataPipeline = class(PipelineBase, IModel)
private
fModel: IUnsupervisedModel;
procedure ValidateNumericFeatures(df: DataFrame);
function TransformToMatrix(df: DataFrame): Matrix;
protected
procedure ValidateSchema(df: DataFrame); override;
public
/// Создаёт пустой конвейер
constructor Create;
/// Добавляет шаг в конец конвейера.
/// Принимает:
/// • IPreprocessor (DataFrame-уровень)
/// • ITransformer (Matrix-уровень)
/// • IUnsupervisedModel (модель, должна быть последней)
function Add(step: IPipelineStep): UDataPipeline;
/// Строит unsupervised-конвейер из шагов обработки данных и модели.
static function Build(features: array of string;
params steps: array of IPipelineStep): UDataPipeline;
/// Обучает конвейер на DataFrame.
function Fit(df: DataFrame): UDataPipeline;
/// Применяет обученные DataFrame-шаги к DataFrame и возвращает новый DataFrame.
function Transform(df: DataFrame): DataFrame;
/// Выполняет Fit и Transform за один проход для DataFrame-режима.
/// Недоступен, если конвейер содержит модель (IModel).
function FitTransform(df: DataFrame): DataFrame;
/// Обучает конвейер и сразу возвращает метки кластеров.
///
/// Выполняет полный pipeline:
/// DataFrame-преобразования → построение матрицы признаков →
/// матричные трансформеры → FitPredict модели.
///
/// Используется для задач кластеризации, где нет разделения на train/test.
///
/// Доступен только если:
/// • конвейер содержит модель кластеризации (IClusterer).
function FitPredict(df: DataFrame): array of integer;
/// Делает предсказание модели для объектов из DataFrame.
function Predict(df: DataFrame): Vector;
/// Возвращает метки кластеров для объектов из DataFrame.
///
/// Выполняет последовательные преобразования данных (DataFrame и матричные шаги),
/// затем применяет обученную модель кластеризации.
///
/// Доступен только если:
/// • конвейер содержит модель (IModel);
/// • модель поддерживает кластеризацию (IClusterer);
/// • конвейер был обучен (Fit).
function PredictLabels(df: DataFrame): array of integer;
function ToString: string; override;
function Clone: IModel;
function Name: string := Self.GetType.Name;
end;
implementation
uses MLExceptions;
uses DataAdapters;
const
ER_PIPELINE_MODIFY_AFTER_FIT =
'Нельзя добавлять шаг после вызова Fit()!!Cannot add step after Fit';
ER_PIPELINE_STEP_NULL =
'Шаг конвейера не может быть nil!!Pipeline step cannot be nil';
ER_DATAPIPE_DF_AFTER_MATRIX =
'Шаг DataFrame не может идти после матричных шагов!!DataFrame step cannot appear after matrix steps';
ER_PIPELINE_MULTIPLE_MODELS =
'В конвейере разрешена только одна модель!!Only one model is allowed in the pipeline';
ER_DATAPIPE_MATRIX_AFTER_MODEL =
'Матричный шаг не может идти после модели!!Matrix step cannot appear after the model';
ER_DATAPIPE_UNKNOWN_STEP_TYPE =
'Неизвестный тип шага конвейера: {0}!!Unknown pipeline step type: {0}';
ER_TO_VECTOR_NON_NUMERIC =
'ToVector: столбец "{0}" содержит нечисловые или NA значения!!' +
'ToVector: column "{0}" contains non-numeric or NA values';
ER_FITTRANSFORM_WITH_MODEL =
'FitTransform недоступен, если конвейер содержит модель!!FitTransform is not available when the pipeline contains a model';
ER_PROBA_NOT_SUPPORTED =
'Модель не поддерживает предсказание вероятностей!!Model does not support probability prediction';
ER_TARGET_EMPTY =
'Имя целевой переменной не задано!!Target column name is not specified';
ER_FEATURES_EMPTY =
'Список признаков пуст!!Feature list must not be empty';
ER_FEATURE_EMPTY =
'Имя признака не может быть пустым!!Feature name cannot be empty';
ER_DATAPIPE_TARGET_IN_FEATURES =
'Целевая переменная "{0}" не должна входить в список признаков!!Target variable "{0}" must not appear in feature list';
ER_TO_MATRIX_NON_NUMERIC =
'Столбец "{0}" содержит нечисловые или NA значения!!Column "{0}" contains non-numeric or NA values';
ER_DATAPIPE_TARGET_NOT_FOUND =
'Целевой столбец "{0}" не найден. Доступные столбцы: {1}!!' +
'Target column "{0}" not found. Available columns: {1}';
ER_DATAPIPE_FEATURE_NOT_FOUND =
'Признак "{0}" не найден в DataFrame. Доступные столбцы: {1}!!'+
'Feature "{0}" not found in DataFrame. Available columns: {1}';
ER_DATAPIPE_DUPLICATE_FEATURE =
'Повторяющийся признак: {0}!!Duplicate feature: {0}';
ER_PIPELINE_FINALFEATURES =
'Внутренняя ошибка pipeline: итоговый набор признаков не определён. Возможно, Fit не был выполнен корректно!!Pipeline internal error: final feature set is not defined. Fit may not have been executed correctly';
ER_PIPELINE_TARGET_REMOVED =
'Целевая переменная "{0}" была удалена на этапе preprocessing pipeline!!Target column "{0}" was removed during pipeline preprocessing';
ER_PIPELINE_NO_FEATURES =
'После preprocessing pipeline не осталось признаков для обучения модели!!No features remain after pipeline preprocessing';
ER_MATRIXSTEP_NO_FIT =
'Шаг матричного конвейера #{0} не поддерживает Fit!!Matrix step #{0} does not support Fit';
ER_Model_NoFit =
'Модель (тип: {0}) не поддерживает Fit!!Model (type: {0}) does not support Fit';
ER_MODEL_NOT_SUPERVISED =
'Модель (тип: {0}) не поддерживает Fit(X, y)!!' +
'Model (type: {0}) does not support Fit(X, y)';
ER_PIPELINE_FEATURE_NOT_FOUND =
'Признак "{0}" отсутствует во входных данных!!' +
'Feature "{0}" not found in input data';
ER_MODEL_NOT_UNSUPERVISED =
'Модель (тип: {0}) не является моделью без учителя!!' +
'Model (type: {0}) is not an unsupervised model';
ER_PIPELINE_FEATURE_NOT_NUMERIC =
'Признак "{0}" имеет тип {1} и должен быть числовым!!Feature "{0}" has type {1} but must be numeric';
ER_MODEL_NOT_CLUSTERER =
'Модель "{0}" не является алгоритмом кластеризации!!Model "{0}" is not a clustering algorithm';
ER_NOT_CLASSIFICATION =
'Операция доступна только для задач классификации!!Operation is only available for classification tasks';
ER_CLASSES_NOT_AVAILABLE =
'Метки классов недоступны. Убедитесь, что конвейер обучен и задача — классификация!!Class labels are not available. Ensure the pipeline is fitted and the task is classification';
ER_LABELENCODER_TARGET_NOT_ALLOWED =
'LabelEncoder нельзя применять к целевой переменной — кодирование выполняется внутри модели!!LabelEncoder cannot be applied to target — encoding is handled internally by the model';
ER_ENCODELABELS_NOT_CATEGORICAL =
'Целевой столбец должен быть категориальным для задач классификации!!Target column must be categorical for classification tasks';
ER_PIPELINE_TARGET_TRANSFORM_NOT_ALLOWED =
'Преобразование целевой переменной "{0}" запрещено в DataPipeline!!' +
'Transformation of target variable "{0}" is not allowed in DataPipeline';
ER_DATAPIPE_INVALID_TASK =
'DataPipeline поддерживает только классификацию и регрессию!!' +
'DataPipeline supports classification and regression only';
ER_PIPELINE_NO_STEPS =
'Pipeline не содержит шагов!!' +
'Pipeline must contain at least one step';
ER_PIPELINE_LAST_NOT_UNSUPERVISED_MODEL =
'Последний шаг Pipeline должен быть unsupervised-моделью!!' +
'Last Pipeline step must be an unsupervised model';
ER_PIPELINE_INVALID_STEP_ORDER =
'Неверный порядок шагов в Pipeline: модель должна быть последней!!' +
'Invalid pipeline step order: model must be the last step';
ER_PIPELINE_LAST_NOT_SUPERVISED_MODEL =
'Последний шаг Pipeline должен быть supervised-моделью!!' +
'Last Pipeline step must be a supervised model';
ER_MODEL_NOT_CLASSIFIER =
'Модель не является классификатором!!' +
'Model is not a classifier';
ER_MODEL_CLONE_TYPE =
'Clone модели вернул неподдерживаемый тип!!Model Clone returned unsupported type';
//-----------------------------
// DataPipeline
//-----------------------------
constructor PipelineBase.Create;
begin
fDataSteps := new List<IPreprocessor>;
fMatrixSteps := new List<ITransformer>;
fFeatures := nil;
fFitted := false;
end;
function PipelineBase.PrepareMatrix(df: DataFrame; var current: DataFrame): Matrix;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
current := df;
// --- schema
ValidateSchema(current);
// --- DataFrame steps
for var i := 0 to fDataSteps.Count - 1 do
begin
fDataSteps[i] := fDataSteps[i].Fit(current);
current := fDataSteps[i].Transform(current);
end;
// --- features
var feats := new List<string>;
foreach var f in fFeatures do
begin
if current.HasColumn(f) then
begin
feats.Add(f);
continue;
end;
foreach var c in current.Schema.ColumnNames do
if c.StartsWith(f + '_') then
if not feats.Contains(c) then
feats.Add(c);
end;
if feats.Count = 0 then
ArgumentError(ER_PIPELINE_NO_FEATURES);
fFinalFeatures := feats.ToArray;
Result := current.ToMatrix(fFinalFeatures);
end;
function PipelineBase.FitTransformMatrix(var X: Matrix; y: Vector): Matrix;
begin
for var i := 0 to fMatrixSteps.Count - 1 do
begin
var t := fMatrixSteps[i];
if y <> nil then
begin
if t is ISupervisedTransformer(var sup) then
fMatrixSteps[i] := sup.Fit(X, y)
else if t is IUnsupervisedTransformer(var unsup) then
fMatrixSteps[i] := unsup.Fit(X)
else
ArgumentError(ER_MATRIXSTEP_NO_FIT, i);
end
else
begin
if t is IUnsupervisedTransformer(var unsup) then
fMatrixSteps[i] := unsup.Fit(X)
else
ArgumentError(ER_MATRIXSTEP_NO_FIT, i);
end;
X := fMatrixSteps[i].Transform(X);
end;
Result := X;
end;
function PipelineBase.TransformMatrix(X: Matrix): Matrix;
begin
foreach var t in fMatrixSteps do
X := t.Transform(X);
Result := X;
end;
//-----------------------------
// DataPipeline
//-----------------------------
constructor DataPipeline.Create;
begin
fDataSteps := new List<IPreprocessor>;
fMatrixSteps := new List<ITransformer>;
fModel := nil;
fTarget := '';
fFeatures := nil;
fFitted := false;
end;
function DataPipeline.Add(step: IPipelineStep): DataPipeline;
begin
// --- global invariants
if step = nil then
ArgumentError(ER_PIPELINE_STEP_NULL);
if fFitted then
Error(ER_PIPELINE_MODIFY_AFTER_FIT);
// --- target protection
// Любой шаг DataFrame, привязанный к одной или нескольким колонкам,
// не должен затрагивать целевую переменную (target).
// Проверка выполняется через интерфейсы IColumnBoundStep / IColumnsBoundStep
// без привязки к конкретным классам.
if step is IColumnBoundStep(var cstep) then
if cstep.ColumnName = fTarget then
ArgumentError(ER_PIPELINE_TARGET_TRANSFORM_NOT_ALLOWED, fTarget);
if step is IColumnsBoundStep(var mstep) then
if fTarget in mstep.Columns then
ArgumentError(ER_PIPELINE_TARGET_TRANSFORM_NOT_ALLOWED, fTarget);
// --- DataFrame step
if step is IPreprocessor then
begin
if (fMatrixSteps.Count > 0) or (fModel <> nil) then
ArgumentError(ER_DATAPIPE_DF_AFTER_MATRIX);
fDataSteps.Add(step as IPreprocessor);
exit(Self);
end;
// --- Matrix transformer
if step is ITransformer then
begin
if fModel <> nil then
ArgumentError(ER_DATAPIPE_MATRIX_AFTER_MODEL);
fMatrixSteps.Add(step as ITransformer);
exit(Self);
end;
// --- Model (обязательно последний шаг)
if step is ISupervisedModel then
begin
if fModel <> nil then
ArgumentError(ER_PIPELINE_MULTIPLE_MODELS);
fModel := step as ISupervisedModel;
exit(Self);
end;
ArgumentError(ER_DATAPIPE_UNKNOWN_STEP_TYPE, step.ToString);
Result := Self;
end;
class function DataPipeline.Build(
task: TaskKind;
target: string;
features: array of string;
params steps: array of IPipelineStep
): DataPipeline;
begin
if not (task in [TaskKind.tkClassification, TaskKind.tkRegression]) then
ArgumentError(ER_DATAPIPE_INVALID_TASK);
if (target = nil) or (target = '') then
ArgumentError(ER_TARGET_EMPTY);
if (features = nil) or (features.Length = 0) then
ArgumentError(ER_FEATURES_EMPTY);
var seen := new HashSet<string>;
foreach var f in features do
begin
if (f = nil) or (f = '') then
ArgumentError(ER_FEATURE_EMPTY);
if f = target then
ArgumentError(ER_DATAPIPE_TARGET_IN_FEATURES, target);
if seen.Contains(f) then
ArgumentError(ER_DATAPIPE_DUPLICATE_FEATURE, f);
seen.Add(f);
end;
if (steps = nil) or (Length(steps) = 0) then
ArgumentError(ER_PIPELINE_NO_STEPS);
var last := steps[High(steps)];
if last = nil then
ArgumentError(ER_PIPELINE_STEP_NULL, High(steps));
if not (last is ISupervisedModel) then
ArgumentError(ER_PIPELINE_LAST_NOT_SUPERVISED_MODEL);
var modelFound := false;
for var i := 0 to High(steps) do
begin
if steps[i] is ISupervisedModel then
begin
if modelFound then
ArgumentError(ER_PIPELINE_MULTIPLE_MODELS);
modelFound := true;
end;
end;
for var i := 0 to High(steps) - 1 do
begin
var step := steps[i];
if step = nil then
ArgumentError(ER_PIPELINE_STEP_NULL, i);
if step is ISupervisedModel then
ArgumentError(ER_PIPELINE_INVALID_STEP_ORDER);
end;
var p := new DataPipeline;
p.fTarget := target;
p.fFeatures := Copy(features);
p.fTask := task;
for var i := 0 to High(steps) do
p.Add(steps[i]);
Result := p;
end;
function DataPipeline.Fit(df: DataFrame): DataPipeline;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if fModel = nil then
ArgumentError(ER_MODEL_NULL);
var current: DataFrame;
var X := PrepareMatrix(df, current);
if not current.HasColumn(fTarget) then
ArgumentError(ER_PIPELINE_TARGET_REMOVED, fTarget);
var classes: array of string;
var y: Vector;
case fTask of
tkRegression:
y := current.ToVector(fTarget);
tkClassification:
begin
var labels := current.EncodeLabels(fTarget, classes);
y := new Vector(labels);
end;
end;
if X.RowCount <> y.Length then
DimensionError(ER_XY_SIZE_MISMATCH, X.RowCount, y.Length);
X := FitTransformMatrix(X, y);
fModel := fModel.Fit(X, y);
if fTask = tkClassification then
begin
if fModel is IClassifier(var cls) then
cls.SetClassLabels(classes)
else
Error(ER_MODEL_NOT_CLASSIFIER);
end;
fFitted := true;
Result := Self;
end;
function DataPipeline.Transform(df: DataFrame): DataFrame;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
var current := df;
foreach var s in fDataSteps do
current := s.Transform(current);
Result := current;
end;
function DataPipeline.FitTransform(df: DataFrame): DataFrame;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if fModel <> nil then
ArgumentError(ER_FITTRANSFORM_WITH_MODEL);
var current := df;
for var i := 0 to fDataSteps.Count - 1 do
begin
fDataSteps[i] := fDataSteps[i].Fit(current);
current := fDataSteps[i].Transform(current);
end;
fFitted := true;
Result := current;
end;
function DataPipeline.Predict(df: DataFrame): Vector;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if fModel = nil then
ArgumentError(ER_MODEL_NULL);
var current := Transform(df);
var X := current.ToMatrix(fFinalFeatures);
X := TransformMatrix(X);
if not (fModel is IPredictiveModel) then
Error(ER_PREDICT_NOT_SUPPORTED);
Result := (fModel as IPredictiveModel).Predict(X);
end;
function DataPipeline.PredictProba(df: DataFrame): Matrix;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if fTask <> tkClassification then
ArgumentError(ER_NOT_CLASSIFICATION);
if not (fModel is IProbabilisticClassifier) then
ArgumentError(ER_PROBA_NOT_SUPPORTED);
var current := Transform(df);
var X := current.ToMatrix(fFinalFeatures);
X := TransformMatrix(X);
Result := (fModel as IProbabilisticClassifier).PredictProba(X);
end;
function DataPipeline.GetClassLabels: array of string;
begin
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if fTask <> tkClassification then
ArgumentError(ER_NOT_CLASSIFICATION);
var cls := fModel as IClassifier;
if cls = nil then
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
Result := cls.GetClassLabels;
end;
procedure DataPipeline.ValidateSchema(df: DataFrame);
begin
if fTarget = '' then
ArgumentError(ER_TARGET_EMPTY);
if (fFeatures = nil) or (Length(fFeatures) = 0) then
ArgumentError(ER_FEATURES_EMPTY);
// target не должен входить в features
for var i := 0 to High(fFeatures) do
if fFeatures[i] = fTarget then
ArgumentError(ER_DATAPIPE_TARGET_IN_FEATURES, fTarget);
// проверка существования target
if not df.Schema.HasColumn(fTarget) then
begin
var cols := df.Schema.ColumnNames.JoinToString(', ');
ArgumentError(ER_DATAPIPE_TARGET_NOT_FOUND, fTarget, cols);
end;
if fTask = tkClassification then
if not df.IsCategorical(fTarget) then
ArgumentError(ER_ENCODELABELS_NOT_CATEGORICAL, fTarget);
var seen := new HashSet<string>;
for var i := 0 to High(fFeatures) do
begin
var f := fFeatures[i];
if f = '' then
ArgumentError(ER_FEATURE_EMPTY);
if not df.Schema.HasColumn(f) then
begin
var cols := df.Schema.ColumnNames.JoinToString(', ');
ArgumentError(ER_DATAPIPE_FEATURE_NOT_FOUND, f, cols);
end;
if seen.Contains(f) then
ArgumentError(ER_DATAPIPE_DUPLICATE_FEATURE, f);
seen.Add(f);
end;
end;
function DataPipeline.ToString: string;
begin
var sb := 'DataPipeline (' +
(if fFitted then 'trained' else 'not trained') + '):' + NewLine;
sb += ' Target: ' + fTarget + NewLine;
sb += ' Features: ' + fFeatures.JoinToString(', ') + NewLine;
var idx := 1;
foreach var s in fDataSteps do
begin
sb += ' [' + idx + '] ' + s.ToString + NewLine;
idx += 1;
end;
foreach var t in fMatrixSteps do
begin
sb += ' [' + idx + '] ' + t.ToString + NewLine;
idx += 1;
end;
if fModel <> nil then
sb += ' [' + idx + '] ' + fModel.ToString;
Result := sb;
end;
function DataPipeline.Clone: IModel;
begin
var p := new DataPipeline;
// --- конфигурация
p.fFeatures := Copy(fFeatures);
p.fTarget := fTarget;
p.fTask := fTask;
// --- шаги (глубокая копия)
p.fDataSteps := fDataSteps.Select(s -> s.Clone).ToList;
p.fMatrixSteps := fMatrixSteps.Select(s -> s.Clone).ToList;
// --- модель (глубокая копия конфигурации)
if fModel <> nil then
begin
var m := fModel.Clone;
if not (m is ISupervisedModel) then
Error(ER_MODEL_CLONE_TYPE);
p.fModel := m as ISupervisedModel;
end;
// --- состояние НЕ копируем
p.fFinalFeatures := nil;
p.fFitted := false;
Result := p;
end;
//-----------------------------
// UDataPipeline
//-----------------------------
constructor UDataPipeline.Create;
begin
fDataSteps := new List<IPreprocessor>;
fMatrixSteps := new List<ITransformer>;
fModel := nil;
fFeatures := nil;
fFitted := false;
end;
function UDataPipeline.Add(step: IPipelineStep): UDataPipeline;
begin
if step = nil then
ArgumentError(ER_PIPELINE_STEP_NULL);
if fFitted then
Error(ER_PIPELINE_MODIFY_AFTER_FIT);
// --- DataFrame step
if step is IPreprocessor then
begin
if (fMatrixSteps.Count > 0) or (fModel <> nil) then
ArgumentError(ER_DATAPIPE_DF_AFTER_MATRIX);
fDataSteps.Add(step as IPreprocessor);
exit(Self);
end;
// --- Matrix transformer
if step is ITransformer then
begin
if fModel <> nil then
ArgumentError(ER_DATAPIPE_MATRIX_AFTER_MODEL);
fMatrixSteps.Add(step as ITransformer);
exit(Self);
end;
// --- Model (обязательно последний шаг)
if step is IUnsupervisedModel then
begin
if fModel <> nil then
ArgumentError(ER_PIPELINE_MULTIPLE_MODELS);
fModel := step as IUnsupervisedModel;
exit(Self);
end;
ArgumentError(ER_DATAPIPE_UNKNOWN_STEP_TYPE, step.ToString);
Result := Self;
end;
class function UDataPipeline.Build(features: array of string;
params steps: array of IPipelineStep): UDataPipeline;
begin
if (features = nil) or (features.Length = 0) then
ArgumentError(ER_FEATURES_EMPTY);
var seen := new HashSet<string>;
foreach var f in features do
begin
if (f = nil) or (f = '') then
ArgumentError(ER_FEATURE_EMPTY);
if seen.Contains(f) then
ArgumentError(ER_DATAPIPE_DUPLICATE_FEATURE, f);
seen.Add(f);
end;
if (steps = nil) or (Length(steps) = 0) then
ArgumentError(ER_PIPELINE_NO_STEPS);
var last := steps[High(steps)];
if last = nil then
ArgumentError(ER_PIPELINE_STEP_NULL, High(steps));
if not (last is IUnsupervisedModel) then
ArgumentError(ER_PIPELINE_LAST_NOT_UNSUPERVISED_MODEL);
for var i := 0 to High(steps) - 1 do
begin
var step := steps[i];
if step = nil then
ArgumentError(ER_PIPELINE_STEP_NULL, i);
if step is IUnsupervisedModel then
ArgumentError(ER_PIPELINE_INVALID_STEP_ORDER);
end;
var p := new UDataPipeline;
p.fFeatures := Copy(features);
for var i := 0 to High(steps) do
p.Add(steps[i]);
Result := p;
end;
function UDataPipeline.Fit(df: DataFrame): UDataPipeline;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if fModel = nil then
ArgumentError(ER_MODEL_NULL);
var current: DataFrame;
var X := PrepareMatrix(df, current);
X := FitTransformMatrix(X);
fModel := fModel.Fit(X);
fFitted := true;
Result := Self;
end;
function UDataPipeline.Transform(df: DataFrame): DataFrame;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
var current := df;
foreach var s in fDataSteps do
current := s.Transform(current);
Result := current;
end;
function UDataPipeline.FitTransform(df: DataFrame): DataFrame;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if fModel <> nil then
ArgumentError(ER_FITTRANSFORM_WITH_MODEL);
var current := df;
for var i := 0 to fDataSteps.Count - 1 do
begin
fDataSteps[i] := fDataSteps[i].Fit(current);
current := fDataSteps[i].Transform(current);
end;
fFitted := true;
Result := current;
end;
function UDataPipeline.FitPredict(df: DataFrame): array of integer;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if fModel = nil then
ArgumentError(ER_MODEL_NULL);
if not (fModel is IClusterer) then
ArgumentError(ER_MODEL_NOT_CLUSTERER, fModel.GetType.Name);
var current := df;
// --- 0) Проверка входной схемы
ValidateSchema(current);
// --- 1) DataFrame шаги
for var i := 0 to fDataSteps.Count - 1 do
begin
fDataSteps[i] := fDataSteps[i].Fit(current);
current := fDataSteps[i].Transform(current);
end;
// --- 2) вычислить финальные признаки
var feats := new List<string>;
foreach var f in fFeatures do
begin
if current.HasColumn(f) then
begin
feats.Add(f);
continue;
end;
// искать производные признаки (например, OneHotEncoder)
foreach var c in current.Schema.ColumnNames do
if c.StartsWith(f + '_') then
if not feats.Contains(c) then
feats.Add(c);
end;
if feats.Count = 0 then
ArgumentError(ER_PIPELINE_NO_FEATURES);
fFinalFeatures := feats.ToArray;
var X := current.ToMatrix(fFinalFeatures);
// --- 3) Matrix transformers
X := FitTransformMatrix(X);
// --- 4) модель
var cl := fModel as IClusterer;
Result := cl.FitPredict(X);
fFitted := true;
end;
procedure UDataPipeline.ValidateNumericFeatures(df: DataFrame);
begin
if fFinalFeatures = nil then
Error(ER_PIPELINE_FINALFEATURES);
var schema := df.Schema;
foreach var f in fFinalFeatures do
begin
var idx := schema.IndexOf(f);
if idx < 0 then
ArgumentError(ER_PIPELINE_FEATURE_NOT_FOUND, f);
var t := schema.ColumnTypeAt(idx);
if (t <> ColumnType.ctInt) and (t <> ColumnType.ctFloat) then
ArgumentError(ER_PIPELINE_FEATURE_NOT_NUMERIC, f, t.ToString);
end;
end;
function UDataPipeline.TransformToMatrix(df: DataFrame): Matrix;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
var current := Transform(df);
ValidateNumericFeatures(current);
if fFinalFeatures = nil then
Error(ER_PIPELINE_FINALFEATURES);
for var i := 0 to High(fFinalFeatures) do
if not current.HasColumn(fFinalFeatures[i]) then
ArgumentError(ER_PIPELINE_FEATURE_NOT_FOUND, fFinalFeatures[i]);
var X := current.ToMatrix(fFinalFeatures);
X := TransformMatrix(X);
Result := X;
end;
function UDataPipeline.Predict(df: DataFrame): Vector;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if fModel = nil then
ArgumentError(ER_MODEL_NULL);
var X := TransformToMatrix(df);
if not (fModel is IPredictiveModel) then
Error(ER_PREDICT_NOT_SUPPORTED);
Result := (fModel as IPredictiveModel).Predict(X);
end;
function UDataPipeline.PredictLabels(df: DataFrame): array of integer;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if fModel = nil then
ArgumentError(ER_MODEL_NULL);
if not (fModel is IPredictiveClusterer) then
Error(ER_PREDICT_NOT_SUPPORTED);
var cl := fModel as IPredictiveClusterer;
var X := TransformToMatrix(df);
Result := cl.PredictLabels(X);
end;
procedure UDataPipeline.ValidateSchema(df: DataFrame);
begin
if (fFeatures = nil) or (Length(fFeatures) = 0) then
ArgumentError(ER_FEATURES_EMPTY);
var seen := new HashSet<string>;
for var i := 0 to High(fFeatures) do
begin
var f := fFeatures[i];
if f = '' then
ArgumentError(ER_FEATURE_EMPTY);
if not df.Schema.HasColumn(f) then
begin
var cols := df.Schema.ColumnNames.JoinToString(', ');
ArgumentError(ER_DATAPIPE_FEATURE_NOT_FOUND, f, cols);
end;
if seen.Contains(f) then
ArgumentError(ER_DATAPIPE_DUPLICATE_FEATURE, f);
seen.Add(f);
end;
end;
function UDataPipeline.ToString: string;
begin
var sb := 'UDataPipeline (' +
(if fFitted then 'trained' else 'not trained') + '):' + NewLine;
var idx := 1;
foreach var s in fDataSteps do
begin
sb += ' [' + idx + '] ' + s.ToString + NewLine;
idx += 1;
end;
foreach var t in fMatrixSteps do
begin
sb += ' [' + idx + '] ' + t.ToString + NewLine;
idx += 1;
end;
if fModel <> nil then
sb += ' [' + idx + '] ' + fModel.ToString;
Result := sb;
end;
function UDataPipeline.Clone: IModel;
begin
var p := new UDataPipeline;
// --- конфигурация
p.fFeatures := Copy(fFeatures);
// --- шаги (глубокая копия)
p.fDataSteps := fDataSteps.Select(s -> s.Clone).ToList;
p.fMatrixSteps := fMatrixSteps.Select(s -> s.Clone).ToList;
// --- модель
if fModel <> nil then
p.fModel := fModel.Clone as IUnsupervisedModel;
// --- состояние НЕ копируем
p.fFinalFeatures := nil;
p.fFitted := false;
Result := p;
end;
end.