MLDatasets.pas

PlotML - новые стандартные ML-модули
This commit is contained in:
Mikhalkovich Stanislav 2026-03-08 18:31:35 +03:00
parent d2fd6c21d6
commit b5730957f8
20 changed files with 2369 additions and 296 deletions

View file

@ -15,7 +15,7 @@ internal static class RevisionClass
public const string Major = "3";
public const string Minor = "11";
public const string Build = "1";
public const string Revision = "3765";
public const string Revision = "3767";
public const string MainVersion = Major + "." + Minor;
public const string FullVersion = Major + "." + Minor + "." + Build + "." + Revision;

View file

@ -1,4 +1,4 @@
%MINOR%=11
%REVISION%=3765
%REVISION%=3767
%COREVERSION%=1
%MAJOR%=3

View file

@ -2,27 +2,27 @@
uses PlotWPF;
begin
var g := new GridWPF(2,2,10);
var c := new LineGraphWPF(0,Pi,v -> v*Sin(v*10));
c.PlotRect := Rect(0,0,10,10);
c.Graph[0].ChangeData(0,Pi,x->x*x);
var g := new GridWPF(2, 2, 10);
var c := new LineGraphWPF(0, Pi, v -> v * Sin(v * 10));
c.PlotRect := Rect(0, 0, 10, 10);
c.Graph[0].ChangeData(0, Pi, x -> x * x);
c.Graph[0].Color := Colors.Green;
c.Graph[0].Thickness := 3;
c.AddLineGraph(0,Pi,v -> Sqrt(v));
c.AddLineGraph(0, Pi, v -> Sqrt(v));
var c2 := new LineGraphWPF(0,Pi,v -> Sin(v*10)-Cos(v*7));
var c2 := new LineGraphWPF(0, Pi, v -> Sin(v * 10) - Cos(v * 7));
var c4 := new MarkerGraphWPF(|1.0,2,3,4,5|,|5.0,15,7,12,2|);
c4.AddLineGraph(|1.0,2,3,4,5|,|5.0+1,15+1,7+1,12+1,2+1|);
c4.AddMarkerGraph(|1.0,2,3,4,5|,|5.0+1,15+1,7+1,12+1,2+1|,Colors.Bisque,MarkerType.Diamond,8);
var c4 := new MarkerGraphWPF([1.0, 2, 3, 4, 5], [5.0, 15, 7, 12, 2]);
c4.AddLineGraph([1.0, 2, 3, 4, 5], [5.0 + 1, 15 + 1, 7 + 1, 12 + 1, 2 + 1]);
c4.AddMarkerGraph([1.0, 2, 3, 4, 5], [5.0 + 1, 15 + 1, 7 + 1, 12 + 1, 2 + 1], Colors.Bisque, MarkerType.Diamond, 8);
c4.Graph[0].Thickness := 0.7;
c4.Graph[1].MarkerType := MarkerType.Box;
c4.Graph[2].Thickness := 0.7;
var gg := new GridWPF(2,2,3);
new LineGraphWPF(0,2,x->Cos(10*x));
new LineGraphWPF(0,2,x->Sqrt(x));
new LineGraphWPF(0,2,x->Sin(10*x));
new LineGraphWPF(0,2,x->exp(x));
var gg := new GridWPF(2, 2, 3);
new LineGraphWPF(0, 2, x -> Cos(10 * x));
new LineGraphWPF(0, 2, x -> Sqrt(x));
new LineGraphWPF(0, 2, x -> Sin(10 * x));
new LineGraphWPF(0, 2, x -> exp(x));
end.

View file

@ -1 +1 @@
3.11.1.3765
3.11.1.3767

View file

@ -1 +1 @@
!define VERSION '3.11.1.3765'
!define VERSION '3.11.1.3767'

View file

@ -21,7 +21,8 @@ uses
TasksArr, TasksMatr, TasksStr, Tasks1Begin, Tasks1BoolIfCase, Tasks1Loops, Tasks1Arr,
WPF,
DataFrameABC, DataFrameABCCore, LinearAlgebraML, PreprocessorABC,
MetricsABC, MLABC, MLCoreABC, MLModelsABC, ValidationML, MLExceptions, InspectionML, MLPipelineABC
MetricsABC, MLABC, MLCoreABC, MLModelsABC, ValidationML, MLExceptions, InspectionML, MLPipelineABC,
PlotML, MLDatasets
;
begin

View file

@ -11,7 +11,8 @@
ABCDatabases,
School, SF, TurtleABC,
DataFrameABC, DataFrameABCCore, LinearAlgebraML, PreprocessorABC,
MetricsABC, MLABC, MLCoreABC, MLModelsABC, ValidationML, MLExceptions, InspectionML, MLPipelineABC
MetricsABC, MLABC, MLCoreABC, MLModelsABC, ValidationML, MLExceptions, InspectionML, MLPipelineABC,
PlotML, MLDatasets
;
begin

View file

@ -191,6 +191,8 @@
File ..\bin\Lib\MLExceptions.pcu
File ..\bin\Lib\InspectionML.pcu
File ..\bin\Lib\MLPipelineABC.pcu
File ..\bin\Lib\PlotML.pcu
File ..\bin\Lib\MLDatasets.pcu
File ..\bin\Lib\PABCRtl.dll
File ..\bin\Lib\HelixToolkit.Wpf.dll
@ -296,6 +298,8 @@
${AddFile} "MLExceptions.pcu"
${AddFile} "InspectionML.pcu"
${AddFile} "MLPipelineABC.pcu"
${AddFile} "PlotML.pcu"
${AddFile} "MLDatasets.pcu"
${AddFile} "turtle.png"
@ -438,6 +442,9 @@
File ..\bin\Lib\MLExceptions.pas
File ..\bin\Lib\InspectionML.pas
File ..\bin\Lib\MLPipelineABC.pas
File ..\bin\Lib\PlotML.pas
File ..\bin\Lib\MLDatasets.pas
File ..\bin\Lib\__RedirectIOMode.vb
File ..\bin\Lib\VBSystem.vb
@ -530,7 +537,8 @@
${AddFile} "MLExceptions.pas"
${AddFile} "InspectionML.pas"
${AddFile} "MLPipelineABC.pas"
${AddFile} "PlotML.pas"
${AddFile} "MLDatasets.pas"
${AddFile} "__RedirectIOMode.vb"
${AddFile} "VBSystem.vb"

View file

@ -122,6 +122,9 @@ copy bin\Lib\ValidationML.pcu Release\PascalABCNETLinux\Lib\ValidationML.pcu
copy bin\Lib\MLExceptions.pcu Release\PascalABCNETLinux\Lib\MLExceptions.pcu
copy bin\Lib\InspectionML.pcu Release\PascalABCNETLinux\Lib\InspectionML.pcu
copy bin\Lib\MLPipelineABC.pcu Release\PascalABCNETLinux\Lib\MLPipelineABC.pcu
copy bin\Lib\MLPipelineABC.pcu Release\PascalABCNETLinux\Lib\MLDatasets.pcu
copy bin\Lib\MLPipelineABC.pcu Release\PascalABCNETLinux\Lib\PlotML.pcu
copy bin\Lib\ABCDatabases.pas Release\PascalABCNETLinux\LibSource\ABCDatabases.pas
copy bin\Lib\BBCMicrobit.pas Release\PascalABCNETLinux\LibSource\BBCMicrobit.pas
@ -175,6 +178,8 @@ copy bin\Lib\ValidationML.pas Release\PascalABCNETLinux\LibSource\ValidationML.p
copy bin\Lib\MLExceptions.pas Release\PascalABCNETLinux\LibSource\MLExceptions.pas
copy bin\Lib\InspectionML.pas Release\PascalABCNETLinux\LibSource\InspectionML.pas
copy bin\Lib\MLPipelineABC.pas Release\PascalABCNETLinux\LibSource\MLPipelineABC.pas
copy bin\Lib\MLPipelineABC.pas Release\PascalABCNETLinux\LibSource\MLDatasets.pas
copy bin\Lib\MLPipelineABC.pas Release\PascalABCNETLinux\LibSource\PlotML.pas
copy bin\Lng\Eng\.LanguageName Release\PascalABCNETLinux\Lng\Eng\.LanguageName

View file

@ -937,7 +937,7 @@ begin
var cols := new Column[schema.ColumnCount];
for var i := 0 to schema.ColumnCount - 1 do
cols[i] := columns[fSchema.IndexOf(schema.Names[i])];
cols[i] := columns[fSchema.IndexOf(schema.ColumnNames[i])];
Result := new DataFrame(cols.ToList, schema);
end;
@ -1649,7 +1649,7 @@ function DataFrame.Mean(colName: string): real
function DataFrame.Median(colIndex: integer): real;
begin
CheckColumnIndex(colIndex);
Result := Statistics.Median(Self, fSchema.Names[colIndex]);
Result := Statistics.Median(Self, fSchema.ColumnNames[colIndex]);
end;
function DataFrame.Median(colName: string): real;
@ -2454,13 +2454,13 @@ begin
begin
case schema.Types[i] of
ctInt:
cols.Add(new IntColumn(schema.Names[i], schema.IsCategorical[i]));
cols.Add(new IntColumn(schema.ColumnNames[i], schema.IsCategorical[i]));
ctFloat:
cols.Add(new FloatColumn(schema.Names[i]));
cols.Add(new FloatColumn(schema.ColumnNames[i]));
ctStr:
cols.Add(new StrColumn(schema.Names[i], schema.IsCategorical[i]));
cols.Add(new StrColumn(schema.ColumnNames[i], schema.IsCategorical[i]));
ctBool:
cols.Add(new BoolColumn(schema.Names[i]));
cols.Add(new BoolColumn(schema.ColumnNames[i]));
end;
end;
@ -3046,12 +3046,12 @@ end;
procedure DataFrame.PrintSchema;
begin
var nameWidth := fSchema.Names.Max(s -> s.Length);
var nameWidth := fSchema.ColumnNames.Max(s -> s.Length);
var typeWidth := 6; // Int / Float / Bool
for var i := 0 to ColumnCount - 1 do
begin
var name := fSchema.Names[i].PadRight(nameWidth);
var name := fSchema.ColumnNames[i].PadRight(nameWidth);
var typ := GetColumnType(i).ToString.Replace('ct','').PadRight(typeWidth);
PABCSystem.Println($'{name} : {typ}');
end;
@ -3062,7 +3062,7 @@ begin
PABCSystem.Println($'Rows : {RowCount}');
PABCSystem.Println($'Columns : {ColumnCount}');
var nameWidth := fSchema.Names.Max(s -> s.Length);
var nameWidth := fSchema.ColumnNames.Max(s -> s.Length);
var typeWidth := 6; // Int / Float / Bool
var infoWidth := nameWidth + 3 + typeWidth + 12;
@ -3070,7 +3070,7 @@ begin
for var i := 0 to ColumnCount - 1 do
begin
var name := fSchema.Names[i].PadRight(nameWidth);
var name := fSchema.ColumnNames[i].PadRight(nameWidth);
var typ := GetColumnType(i).ToString.Replace('ct','').PadRight(typeWidth);
var cnt := Count(i);
PABCSystem.Println($'{name} : {typ} ({cnt} non-NA)');
@ -3695,7 +3695,7 @@ begin
// выбираем только числовые столбцы
for var i := 0 to df.ColumnCount - 1 do
if df.GetColumnType(i) in [ColumnType.ctInt, ColumnType.ctFloat] then
names.Add(df.fSchema.Names[i]);
names.Add(df.fSchema.ColumnNames[i]);
var n := names.Count;
if n = 0 then
@ -3758,7 +3758,7 @@ begin
means[i] := df.Mean(i);
stds[i] := df.Std(i);
if stds[i] = 0 then
Error(ER_ZERO_STD_COLUMN, df.fSchema.Names[i]);
Error(ER_ZERO_STD_COLUMN, df.fSchema.ColumnNames[i]);
isNumeric[i] := true;
end;
end;
@ -3767,7 +3767,7 @@ begin
for var i := 0 to df.ColumnCount - 1 do
begin
if isNumeric[i] then
res.AddFloatColumn(df.fSchema.Names[i], new real[df.RowCount], nil)
res.AddFloatColumn(df.fSchema.ColumnNames[i], new real[df.RowCount], nil)
else
res.AddColumnView(df.columns[i]); // private helper
end;
@ -3839,7 +3839,7 @@ begin
begin
var (mn, mx) := df.MinMax(i);
if mn = mx then
Error(ER_ZERO_RANGE_COLUMN, df.fSchema.Names[i]);
Error(ER_ZERO_RANGE_COLUMN, df.fSchema.ColumnNames[i]);
mins[i] := mn;
maxs[i] := mx;
isNumeric[i] := true;
@ -3850,7 +3850,7 @@ begin
for var i := 0 to df.ColumnCount - 1 do
begin
if isNumeric[i] then
res.AddFloatColumn(df.fSchema.Names[i], new real[df.RowCount], nil)
res.AddFloatColumn(df.fSchema.ColumnNames[i], new real[df.RowCount], nil)
else
res.AddColumnView(df.columns[i]); // private helper
end;

View file

@ -22,7 +22,7 @@ type
class function BuildIndex(names: array of string): Dictionary<string, integer>;
public
property ColumnCount: integer read fNames.Length;
property Names: array of string read fNames;
property ColumnNames: array of string read fNames;
property Types: array of ColumnType read fTypes;
property IsCategorical: array of boolean read fIsCategorical;

View file

@ -19,6 +19,8 @@ type
function Clone: Vector;
function ToArray: array of real;
static function operator +(a, b: Vector): Vector;
static function operator -(a, b: Vector): Vector;
@ -82,6 +84,9 @@ type
constructor Create(r, c: integer);
constructor Create(values: array[,] of real);
function ToArray2D: array[,] of real;
function RowToArray(r: integer): array of real;
function Clone: Matrix;
function ColumnSums: Vector;
@ -320,6 +325,11 @@ begin
data := values.Select(x -> real(x)).ToArray;
end;
function Vector.ToArray: array of real;
begin
Result := Copy(data);
end;
function Vector.Clone: Vector;
begin
Result := new Vector(data);
@ -492,6 +502,16 @@ begin
data := Copy(values);
end;
function Matrix.ToArray2D: array[,] of real;
begin
Result := Copy(data);
end;
function Matrix.RowToArray(r: integer): array of real;
begin
Result := data.Row(r);
end;
function Matrix.Clone: Matrix;
begin
Result := new Matrix(data);

View file

@ -57,6 +57,8 @@ type
GradientBoostingClassifier = MLModelsABC.GradientBoostingClassifier;
KNNClassifier = MLModelsABC.KNNClassifier;
KNNRegressor = MLModelsABC.KNNRegressor;
KMeans = MLModelsABC.KMeans;
DBSCAN = MLModelsABC.DBSCAN;
KNNWeighting = MLModelsABC.KNNWeighting;
TGBLoss = MLModelsABC.TGBLoss;
@ -68,6 +70,12 @@ type
Inspection = InspectionML.Inspection;
IPreprocessor = PreprocessorABC.IPreprocessor;
LabelEncoder = PreprocessorABC.LabelEncoder;
OneHotEncoder = PreprocessorABC.OneHotEncoder;
ImputeStrategy = PreprocessorABC.ImputeStrategy;
Imputer = PreprocessorABC.Imputer;
implementation

View file

@ -28,6 +28,22 @@ type
function Clone: IModel;
end;
/// Интерфейс модели без учителя (Unsupervised Model).
/// Наследуется от базового интерфейса IModel.
/// Предназначен для алгоритмов, обучающихся только по признакам X
/// без использования целевых значений (y)
IUnsupervisedModel = interface(IModel)
procedure Fit(X: Matrix);
end;
/// Интерфейс алгоритма кластеризации.
/// Наследуется от IUnsupervisedModel.
/// Предназначен для моделей, разбивающих объекты на кластеры
/// и возвращающих индекс кластера для каждого объекта
IClusterer = interface(IUnsupervisedModel)
function Predict(X: Matrix): Vector;
end;
/// Интерфейс древовидной модели машинного обучения
ITreeModel = interface(IModel)
function FeatureImportances: Vector;
@ -35,8 +51,7 @@ type
/// Интерфейс классификатора.
/// Наследуется от IModel.
/// Предназначен для моделей, выполняющих классификацию
/// (предсказание меток классов).
/// Предназначен для моделей, выполняющих классификацию (предсказание меток классов).
IClassifier = interface(IModel)
end;

138
bin/Lib/MLDatasets.pas Normal file
View file

@ -0,0 +1,138 @@
unit MLDatasets;
interface
uses DataFrameABC, LinearAlgebraML;
type
Datasets = static class
public
// --- synthetic datasets (Matrix + Vector)
/// Генерирует k кластеров (blobs) для задач классификации и кластеризации
static function MakeBlobs(
n: integer := 300;
centers: integer := 3;
seed: integer := 1): (Matrix, Vector);
/// Генерирует датасет "две луны" для демонстрации нелинейной классификации и кластеризации
static function MakeMoons(
n: integer := 300;
noise: real := 0.05;
seed: integer := 1): (Matrix, Vector);
/// Генерирует синтетический датасет для задач регрессии
static function MakeRegression(
n: integer := 300;
noise: real := 0.1;
seed: integer := 1): (Matrix, Vector);
/// Генерирует концентрические круги (сложная нелинейная классификация)
static function MakeCircles(
n: integer := 300;
noise: real := 0.05;
seed: integer := 1): (Matrix, Vector);
/// Генерирует спиральный датасет (очень сложная нелинейная классификация)
static function MakeSpiral(
n: integer := 300;
classes: integer := 2;
seed: integer := 1): (Matrix, Vector);
// --- DataFrame datasets (реалистичные таблицы)
/// Датасет цен на квартиры (задача регрессии)
static function RussianHousing: DataFrame;
/// Датасет результатов экзамена студентов (классификация)
static function StudentExam: DataFrame;
/// Датасет банковских клиентов (классификация одобрения кредита)
static function BankClients: DataFrame;
/// Датасет поездок такси (регрессия стоимости поездки)
static function TaxiTrips: DataFrame;
/// Датасет транспортной активности пассажиров (кластеризация)
static function MoscowTransport: DataFrame;
/// Датасет интернет-покупок пользователей (классификация покупки)
static function OnlineShopping: DataFrame;
end;
implementation
static function Datasets.MakeBlobs(
n: integer ;
centers: integer ;
seed: integer): (Matrix, Vector);
begin
Result := nil;
end;
static function Datasets.MakeMoons(
n: integer ;
noise: real ;
seed: integer): (Matrix, Vector);
begin
Result := nil;
end;
static function Datasets.MakeRegression(
n: integer ;
noise: real ;
seed: integer): (Matrix, Vector);
begin
Result := nil;
end;
static function Datasets.MakeCircles(
n: integer ;
noise: real ;
seed: integer): (Matrix, Vector);
begin
Result := nil;
end;
static function Datasets.MakeSpiral(
n: integer ;
classes: integer ;
seed: integer): (Matrix, Vector);
begin
Result := nil;
end;
static function Datasets.RussianHousing: DataFrame;
begin
Result := nil;
end;
static function Datasets.StudentExam: DataFrame;
begin
Result := nil;
end;
static function Datasets.BankClients: DataFrame;
begin
Result := nil;
end;
static function Datasets.TaxiTrips: DataFrame;
begin
Result := nil;
end;
static function Datasets.MoscowTransport: DataFrame;
begin
Result := nil;
end;
static function Datasets.OnlineShopping: DataFrame;
begin
Result := nil;
end;
end.

View file

@ -213,6 +213,9 @@ type
fTol: real := 1e-6;
fCheckConvergence: boolean := true;
fMinImprovement: real := 1e-8;
function GetWeights: Matrix;
function GetIntercept: Vector;
public
/// lambda — коэффициент L2-регуляризации.
/// lr — шаг градиентного спуска.
@ -244,6 +247,12 @@ type
/// Создает глубокую копию модели.
function Clone: IModel;
/// Матрица коэффициентов модели (p × k).
property Weights: Matrix read GetWeights;
/// Вектор свободных членов (bias) для каждого класса.
property Intercept: Vector read GetIntercept;
end;
DecisionTreeNode = class
@ -1099,6 +1108,7 @@ type
function Clone: IModel; override;
end;
/// Регрессор на основе алгоритма k ближайших соседей (kNN).
/// Поддерживает равномерное (Uniform) усреднение и взвешенное по расстоянию (Distance) усреднение.
/// Предсказание вычисляется как среднее (или взвешенное среднее) значений целевой переменной
@ -1125,6 +1135,125 @@ type
function Clone: IModel; override;
end;
/// Модель кластеризации методом k-средних (KMeans).
/// Разбивает объекты на k кластеров на основе евклидова расстояния.
/// Реализует алгоритм без учителя
KMeans = class(IClusterer)
private
fNClusters: integer;
fMaxIter: integer;
fTol: real;
fNInit: integer;
fSeed: integer;
fIsFitted: boolean;
fFeatureCount: integer;
fCenters: Matrix;
fInertia: real;
fIterations: integer;
fHasConverged: boolean;
function RunSingle(X: Matrix; rnd: System.Random): (Matrix, real, integer, boolean);
public
/// Создаёт модель KMeans.
/// nClusters — количество кластеров.
/// maxIter — максимальное число итераций.
/// tol — порог сходимости.
/// nInit — количество независимых запусков.
/// seed — значение генератора случайных чисел
constructor Create(
nClusters: integer;
maxIter: integer := 300;
tol: real := 1e-4;
nInit: integer := 10;
seed: integer := -1
);
/// Обучает модель по матрице признаков X.
/// Выполняет nInit запусков и выбирает решение с минимальной инерцией.
procedure Fit(X: Matrix);
function Fit(X: Matrix; y: Vector): IModel;
/// Возвращает индекс кластера для каждого объекта из X.
/// Требует предварительного вызова Fit.
function Predict(X: Matrix): Vector;
/// Выполняет обучение и сразу возвращает метки кластеров.
function FitPredict(X: Matrix): Vector;
/// Создаёт глубокую копию модели.
function Clone: IModel;
/// Количество кластеров.
property NClusters: integer read fNClusters;
/// Максимальное число итераций.
property MaxIter: integer read fMaxIter;
/// Порог сходимости.
property Tol: real read fTol;
/// Количество запусков алгоритма.
property NInit: integer read fNInit;
/// Используемый seed.
property Seed: integer read fSeed;
/// Матрица центроидов (k × p).
property ClusterCenters: Matrix read fCenters;
/// Значение инерции (сумма квадратов расстояний до центроидов).
property Inertia: real read fInertia;
/// Число выполненных итераций.
property Iterations: integer read fIterations;
/// Признак сходимости алгоритма.
property HasConverged: boolean read fHasConverged;
/// Число признаков модели.
property FeatureCount: integer read fFeatureCount;
end;
DBSCAN = class(IClusterer)
private
fEps: real;
fMinSamples: integer;
fSeed: integer;
fIsFitted: boolean;
fFeatureCount: integer;
fLabels: Vector;
fClusterCount: integer;
function RegionQuery(X: Matrix; i: integer; neighbors: List<integer>): integer;
public
/// Создаёт модель DBSCAN.
/// eps — радиус соседства.
/// minSamples — минимальное число точек в eps-окрестности.
/// seed — параметр для совместимости API.
constructor Create(
eps: real;
minSamples: integer := 5;
seed: integer := -1
);
/// Обучает модель на матрице признаков.
procedure Fit(X: Matrix);
/// Совместимость с интерфейсом IModel.
function Fit(X: Matrix; y: Vector): IModel;
/// Возвращает метки кластеров.
function Predict(X: Matrix): Vector;
/// Возвращает метки после обучения.
function FitPredict(X: Matrix): Vector;
/// Копия модели.
function Clone: IModel;
property Labels: Vector read fLabels;
property ClusterCount: integer read fClusterCount;
end;
{$endregion Models}
{$region Pipeline}
@ -1153,6 +1282,63 @@ type
/// Создаёт пустой пайплайн (конвейер машинного обучения).
/// Модель должна быть установлена через SetModel.
constructor Create;
{
// Pipeline.Build используется, когда данные уже представлены
// в виде числовой матрицы признаков X и вектора целевой переменной y.
// В этом случае DataFrame и препроцессоры уровня таблицы не требуются.
//
// Типичные ситуации:
// • экспериментирование с ML-алгоритмами
// • сравнение моделей
// • кросс-валидация
// • подбор гиперпараметров
// • тестирование моделей
//
// Pipeline объединяет несколько матричных преобразований (ITransformer)
// и модель (IModel) в единый объект, который можно обучать и использовать
// для предсказаний.
//
// В отличие от этого, DataPipeline.Build используется,
// когда исходные данные представлены в виде DataFrame
// и требуется выполнить препроцессинг таблицы
// (Imputer, OneHotEncoder, LabelEncoder и др.)
// перед преобразованием данных в Matrix/Vector.
var pipe1 :=
Pipeline.Build(
new StandardScaler,
new LogisticRegression
);
var pipe2 :=
Pipeline.Build(
new StandardScaler,
new PCATransformer(2),
new LogisticRegression
);
var score1 :=
Validation.CrossValidate(
pipe1,
X,
y,
5,
Metrics.Accuracy
);
var score2 :=
Validation.CrossValidate(
pipe2,
X,
y,
5,
Metrics.Accuracy
);
Println('Scaler + LogisticRegression = ', score1);
Println('Scaler + PCA + LogisticRegression = ', score2);
}
/// Строит конвейер машинного обучения из последовательности шагов.
/// Шаги указываются в порядке выполнения:
@ -1193,6 +1379,7 @@ type
{$endregion Pipeline}
{$region Transformers}
/// Стандартизирует признаки: вычитает среднее
/// и делит на стандартное отклонение по каждому столбцу.
/// Используется для приведения признаков к сопоставимому масштабу
@ -1632,8 +1819,16 @@ const
'minSamplesLeaf ({0}) must be less than minSamplesSplit ({1}).';
ER_OOB_NOT_ENABLED =
'OOB score не включен для этой модели. Установите computeOOB = true в конструкторе.!!' +
'OOB score is not enabled for this model. Set computeOOB = true in the constructor.';
'OOB score is not enabled for this model. Set computeOOB = true in the constructor.';
ER_NINIT_INVALID =
'Некорректное значение nInit: {0}. Должно быть не меньше 1!!Invalid nInit value: {0}. Must be at least 1';
ER_INVALID_VALUE_AT =
'Некорректное значение в {0} на позиции {1}!!Invalid value in {0} at index {1}';
ER_EPS_INVALID =
'eps должен быть положительным!!eps must be positive';
ER_MINSAMPLES_INVALID =
'minSamples должен быть >= 1!!minSamples must be >= 1';
{$endregion ErrConstants}
//-----------------------------
@ -2427,6 +2622,22 @@ begin
Result := m;
end;
function LogisticRegression.GetWeights: Matrix;
begin
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
Result := fW;
end;
function LogisticRegression.GetIntercept: Vector;
begin
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
Result := fIntercept;
end;
function GiniCriterion.Impurity(y: Vector; indices: array of integer): real;
begin
var n := indices.Length;
@ -6435,6 +6646,478 @@ begin
Result := clone;
end;
//-----------------------------
// KMeans
//-----------------------------
constructor KMeans.Create(
nClusters: integer;
maxIter: integer;
tol: real;
nInit: integer;
seed: integer
);
begin
if nClusters < 1 then
ArgumentOutOfRangeError(ER_K_INVALID, nClusters);
if maxIter < 1 then
ArgumentOutOfRangeError(ER_MAXITER_INVALID, maxIter);
if tol <= 0 then
ArgumentOutOfRangeError(ER_TOL_INVALID, tol);
if nInit < 1 then
ArgumentOutOfRangeError(ER_NINIT_INVALID, nInit);
fNClusters := nClusters;
fMaxIter := maxIter;
fTol := tol;
fNInit := nInit;
fSeed := seed;
fIsFitted := False;
fFeatureCount := 0;
fCenters := nil;
fInertia := 0.0;
fIterations := 0;
fHasConverged := False;
end;
function KMeans.RunSingle(X: Matrix; rnd: System.Random): (Matrix, real, integer, boolean);
begin
var n := X.RowCount;
var p := X.ColCount;
var k := fNClusters;
// --- 1. Инициализация центроидов (случайные строки X)
var idx := Arr(0..n-1);
idx.Shuffle(rnd);
var centers := new Matrix(k, p);
for var c := 0 to k - 1 do
begin
var r := idx[c];
for var j := 0 to p - 1 do
centers[c,j] := X[r,j];
end;
var inertia := 0.0;
var converged := False;
var iterations := 0;
// --- 2. Основной цикл
for var iter := 1 to fMaxIter do
begin
iterations := iter;
// Assignment + накопление сумм
var counts := new integer[k];
var sums := new Matrix(k, p);
inertia := 0.0;
for var i := 0 to n - 1 do
begin
var bestC := 0;
var bestDist := double.MaxValue;
for var c := 0 to k - 1 do
begin
var dist := 0.0;
for var j := 0 to p - 1 do
begin
var xij := X[i,j];
if double.IsNaN(xij) or double.IsInfinity(xij) then
ArgumentError(ER_INVALID_VALUE_AT, 'X', i);
var cj := centers[c,j];
if double.IsNaN(cj) or double.IsInfinity(cj) then
ArgumentError(ER_INVALID_VALUE_AT, 'Centers', c);
var d := xij - cj;
dist += d * d;
end;
if dist < bestDist then
begin
bestDist := dist;
bestC := c;
end;
end;
inertia += bestDist;
counts[bestC] += 1;
for var j := 0 to p - 1 do
sums[bestC,j] += X[i,j];
end;
// --- 3. Пересчёт центроидов
var maxShift := 0.0;
for var c := 0 to k - 1 do
begin
if counts[c] = 0 then
begin
// Пустой кластер — переинициализация случайной точкой
var r := rnd.Next(n);
for var j := 0 to p - 1 do
centers[c,j] := X[r,j];
continue;
end;
for var j := 0 to p - 1 do
begin
var newVal := sums[c,j] / counts[c];
var shift := Abs(newVal - centers[c,j]);
if shift > maxShift then
maxShift := shift;
centers[c,j] := newVal;
end;
end;
if maxShift < fTol then
begin
converged := True;
break;
end;
end;
Result := (centers, inertia, iterations, converged);
end;
procedure KMeans.Fit(X: Matrix);
begin
if X = nil then
ArgumentNullError(ER_ARG_NULL, 'X');
var n := X.RowCount;
var p := X.ColCount;
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'KMeans');
if p = 0 then
ArgumentError(ER_EMPTY_DATA, 'KMeans');
if fNClusters > n then
ArgumentOutOfRangeError(ER_K_INVALID, fNClusters);
fFeatureCount := p;
var actualSeed :=
if fSeed >= 0 then fSeed
else System.Environment.TickCount and integer.MaxValue;
var rndBase := new System.Random(actualSeed);
var bestInertia := 1e308;
var bestCenters: Matrix := nil;
var bestIterations := 0;
var bestConverged := False;
for var run := 1 to fNInit do
begin
var runSeed := rndBase.Next;
var rnd := new System.Random(runSeed);
var (centers, inertia, iters, converged) := RunSingle(X, rnd);
if inertia < bestInertia then
begin
bestInertia := inertia;
bestCenters := centers;
bestIterations := iters;
bestConverged := converged;
end;
end;
fCenters := bestCenters;
fInertia := bestInertia;
fIterations := bestIterations;
fHasConverged := bestConverged;
fIsFitted := True;
end;
function KMeans.Fit(X: Matrix; y: Vector): IModel;
begin
if X = nil then
ArgumentNullError(ER_ARG_NULL, 'X');
if y <> nil then
if X.RowCount <> y.Length then
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
Fit(X);
Result := Self;
end;
function KMeans.Predict(X: Matrix): Vector;
begin
if not fIsFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if X = nil then
ArgumentNullError(ER_ARG_NULL, 'X');
if X.ColCount <> fFeatureCount then
DimensionError(ER_DIM_MISMATCH, X.ColCount, fFeatureCount);
var n := X.RowCount;
var p := X.ColCount;
var k := fNClusters;
Result := new Vector(n);
for var i := 0 to n - 1 do
begin
var bestC := 0;
var bestDist := 1e308;
for var c := 0 to k - 1 do
begin
var dist := 0.0;
for var j := 0 to p - 1 do
begin
var xij := X[i,j];
if double.IsNaN(xij) or double.IsInfinity(xij) then
ArgumentError(ER_INVALID_VALUE_AT, 'X', i);
var cj := fCenters[c,j];
if double.IsNaN(cj) or double.IsInfinity(cj) then
ArgumentError(ER_INVALID_VALUE_AT, 'Centers', c);
var d := xij - cj;
dist += d * d;
end;
if dist < bestDist then
begin
bestDist := dist;
bestC := c;
end;
end;
Result[i] := bestC; // 0..k-1
end;
end;
function KMeans.FitPredict(X: Matrix): Vector;
begin
Fit(X);
Result := Predict(X);
end;
function KMeans.Clone: IModel;
begin
var km := new KMeans(
fNClusters,
fMaxIter,
fTol,
fNInit,
fSeed
);
km.fIsFitted := fIsFitted;
km.fFeatureCount := fFeatureCount;
km.fInertia := fInertia;
km.fIterations := fIterations;
km.fHasConverged := fHasConverged;
if fCenters <> nil then
begin
var k := fCenters.RowCount;
var p := fCenters.ColCount;
km.fCenters := new Matrix(k, p);
for var i := 0 to k - 1 do
for var j := 0 to p - 1 do
km.fCenters[i,j] := fCenters[i,j];
end;
Result := km;
end;
//-----------------------------
// DBSCAN
//-----------------------------
constructor DBSCAN.Create(
eps: real;
minSamples: integer;
seed: integer
);
begin
if eps <= 0 then
ArgumentOutOfRangeError(ER_EPS_INVALID, eps);
if minSamples < 1 then
ArgumentOutOfRangeError(ER_MINSAMPLES_INVALID, minSamples);
fEps := eps;
fMinSamples := minSamples;
fSeed := seed;
fIsFitted := False;
end;
function DBSCAN.RegionQuery(X: Matrix; i: integer; neighbors: List<integer>): integer;
begin
var n := X.RowCount;
var p := X.ColCount;
var eps2 := fEps * fEps;
neighbors.Clear;
for var j := 0 to n-1 do
begin
var dist := 0.0;
for var k := 0 to p-1 do
begin
var d := X[i,k] - X[j,k];
dist += d*d;
end;
if dist <= eps2 then
neighbors.Add(j);
end;
Result := neighbors.Count;
end;
procedure DBSCAN.Fit(X: Matrix);
begin
if X = nil then
ArgumentNullError(ER_ARG_NULL, 'X');
var n := X.RowCount;
var p := X.ColCount;
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'DBSCAN');
fFeatureCount := p;
var labels := new integer[n];
for var i := 0 to n-1 do
labels[i] := -1; // noise
var visited := new boolean[n];
var neighbors := new List<integer>;
var queue := new Queue<integer>;
var clusterId := 0;
for var i := 0 to n-1 do
begin
if visited[i] then
continue;
visited[i] := True;
var count := RegionQuery(X, i, neighbors);
if count < fMinSamples then
continue;
labels[i] := clusterId;
queue.Clear;
foreach var j in neighbors do
queue.Enqueue(j);
while queue.Count > 0 do
begin
var q := queue.Dequeue;
if not visited[q] then
begin
visited[q] := True;
var count2 := RegionQuery(X, q, neighbors);
if count2 >= fMinSamples then
foreach var t in neighbors do
queue.Enqueue(t);
end;
if labels[q] = -1 then
labels[q] := clusterId;
end;
clusterId += 1;
end;
fClusterCount := clusterId;
fLabels := new Vector(n);
for var i := 0 to n-1 do
fLabels[i] := labels[i];
fIsFitted := True;
end;
function DBSCAN.Fit(X: Matrix; y: Vector): IModel;
begin
if y <> nil then
if X.RowCount <> y.Length then
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
Fit(X);
Result := Self;
end;
function DBSCAN.Predict(X: Matrix): Vector;
begin
if not fIsFitted then
NotFittedError(ER_FIT_NOT_CALLED);
Result := fLabels;
end;
function DBSCAN.FitPredict(X: Matrix): Vector;
begin
Fit(X);
Result := fLabels;
end;
function DBSCAN.Clone: IModel;
begin
var m := new DBSCAN(fEps, fMinSamples, fSeed);
m.fIsFitted := fIsFitted;
m.fFeatureCount := fFeatureCount;
m.fClusterCount := fClusterCount;
if fLabels <> nil then
begin
var n := fLabels.Length;
m.fLabels := new Vector(n);
for var i := 0 to n-1 do
m.fLabels[i] := fLabels[i];
end;
Result := m;
end;
//-----------------------------
// Pipeline
//-----------------------------

View file

@ -43,6 +43,82 @@ type
/// Запрещено добавлять шаги после вызова Fit/FitTransform.
function Add(step: IPipelineStep): DataPipeline;
{ // Примеры использования препроцессоров и моделей в DataPipeline.
// Препроцессоры работают на уровне DataFrame.
// После них Pipeline автоматически преобразует данные в Matrix/Vector.
// Далее выполняются матричные трансформеры и модель.
// ------------------------------------------------------------
// Пример 1. Классификация с категориальным целевым признаком
var pipe :=
DataPipeline.Build(
'species',
['length','width'],
new LabelEncoder('species'), // DataFrame-препроцессор (строки → числа)
new StandardScaler, // матричный трансформер
new LogisticRegression // модель
);
// ------------------------------------------------------------
// Пример 2. Регрессия с пропущенными значениями и категориальным признаком
var pipe :=
DataPipeline.Build(
'price',
['area','floor','district'],
new Imputer('area'), // DataFrame-препроцессор (заполнение NA)
new OneHotEncoder('district'), // DataFrame-препроцессор
new RandomForestRegressor // модель
);
// ------------------------------------------------------------
// Пример 3. Сложный pipeline
var pipe :=
DataPipeline.Build(
'target',
['f1','f2','f3','category'],
new Imputer('f2'), // DataFrame-препроцессор
new OneHotEncoder('category'), // DataFrame-препроцессор
new StandardScaler, // матричный трансформер
new PCATransformer(2), // матричный трансформер
new GradientBoostingRegressor // модель
);
// ------------------------------------------------------------
// Пример 4. Классификация без Pipeline
var df := Datasets.Flowers;
var enc := new LabelEncoder('species');
df := enc.FitTransform(df); // DataFrame уровень
var (X,y) := df.ToXY(['length','width'],'species');
var scaler := new StandardScaler;
X := scaler.FitTransform(X); // Matrix уровень
var model := new LogisticRegression;
model.Fit(X,y);
// Pipeline.Build используется, когда данные уже представлены
// в виде числовой матрицы признаков X и вектора целевой переменной y.
// В этом случае DataFrame и препроцессоры уровня таблицы не требуются.
//
// Типичные ситуации:
// • экспериментирование с ML-алгоритмами
// • сравнение моделей
// • кросс-валидация
// • подбор гиперпараметров
// • тестирование моделей
// Пример 5. Pipeline на матричном уровне
var pipe :=
Pipeline.Build(
new StandardScaler,
new PCATransformer(2),
new LogisticRegression
);
}
/// Строит конвейер из последовательности шагов.
/// target и features используются только в режиме с моделью (когда в шагах присутствует IModel).
/// Шаги должны удовлетворять порядку: DataFrame* → Matrix* → Model.
@ -78,6 +154,8 @@ type
/// Признак того, что был вызван Fit или FitTransform.
property IsFitted: boolean read fFitted;
function ToString: string; override;
end;
implementation
@ -110,14 +188,19 @@ const
'Имя целевой переменной не задано!!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 =
'Целевая переменная не должна входить в список признаков!!Target column must not be included in feature list';
'Целевая переменная "{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}" не найден!!Target column "{0}" not found';
ER_DATAPIPE_FEATURE_NOT_FOUND =
'Столбец признака "{0}" не найден!!Feature column "{0}" not found';
'Признак "{0}" не найден в DataFrame. Доступные столбцы: {1}!!'+
'Feature "{0}" not found in DataFrame. Available columns: {1}';
ER_DATAPIPE_DUPLICATE_FEATURE =
'Повторяющийся признак: {0}!!Duplicate feature: {0}';
function ToMatrix(Self: DataFrame; colNames: array of string): Matrix; extensionmethod;
begin
@ -187,26 +270,19 @@ begin
if fFitted then
Error(ER_PIPELINE_MODIFY_AFTER_FIT);
if step is IPreprocessor then // Data Step
// --- 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;
if step is ITransformer then // Matrix Step
// --- Matrix transformer
if step is ITransformer then
begin
if step is IModel then
begin
if fModel <> nil then
ArgumentError(ER_PIPELINE_MULTIPLE_MODELS);
fModel := step as IModel;
exit(Self);
end;
if fModel <> nil then
ArgumentError(ER_DATAPIPE_MATRIX_AFTER_MODEL);
@ -214,13 +290,45 @@ begin
exit(Self);
end;
ArgumentError(ER_DATAPIPE_UNKNOWN_STEP);
// --- Model (обязательно последний шаг)
if step is IModel then
begin
if fModel <> nil then
ArgumentError(ER_PIPELINE_MULTIPLE_MODELS);
fModel := step as IModel;
exit(Self);
end;
ArgumentError(ER_DATAPIPE_UNKNOWN_STEP + ' ' + step.ToString);
Result := Self;
end;
end;
class function DataPipeline.Build(target: string;
features: array of string; params steps: array of IPipelineStep): DataPipeline;
begin
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;
var p := new DataPipeline;
p.fTarget := target;
p.fFeatures := features;
@ -233,6 +341,9 @@ end;
function DataPipeline.Fit(df: DataFrame): DataPipeline;
begin
if fModel = nil then
Error(ER_MODEL_NULL);
var current := df;
// 1) DataFrame слой (с сохранением fitted-объектов)
@ -355,18 +466,66 @@ begin
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);
// проверка существования target
if not df.Schema.HasColumn(fTarget) then
ArgumentError(ER_DATAPIPE_TARGET_NOT_FOUND, fTarget);
begin
var cols := df.Schema.ColumnNames.JoinToString(', ');
ArgumentError(ER_DATAPIPE_TARGET_NOT_FOUND, fTarget, cols);
end;
var seen := new HashSet<string>;
for var i := 0 to High(fFeatures) do
if not df.Schema.HasColumn(fFeatures[i]) then
ArgumentError(ER_DATAPIPE_FEATURE_NOT_FOUND, fFeatures[i]);
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;
end.

View file

@ -91,45 +91,82 @@ type
/// и Specificity (доли истинно отрицательных).
/// Устойчива к дисбалансу классов.
static function BalancedAccuracy(yTrue, yPred: Vector): real;
/// Вычисляет силуэт (Silhouette) для каждой точки.
/// Для каждой строки матрицы X возвращает значение s(i) в диапазоне [-1, 1].
/// Требует как минимум 2 различных кластера.
/// Использует евклидово расстояние (квадратный корень не извлекается).
/// Сложность O(n²) - для умеренных размеров данных.
static function SilhouetteSamples(X: Matrix; labels: Vector): Vector;
/// Вычисляет средний силуэт (Silhouette Score).
/// Возвращает среднее значение Silhouette по всем точкам.
/// Чем ближе к 1 — тем лучше разделены кластеры.
/// Значение около 0 означает пересечение кластеров.
/// Отрицательное значение указывает на возможную ошибку кластеризации
static function SilhouetteScore(X: Matrix; labels: Vector): real;
/// Вычисляет индекс Калински–Харабаса (Calinski–Harabasz Index).
/// Возвращает отношение межкластерной дисперсии к внутрикластерной дисперсии.
/// Чем больше значение — тем лучше разделены кластеры.
/// Требует как минимум 2 кластера и n > k.
/// Сложность O(n·p).
static function CalinskiHarabaszScore(X: Matrix; labels: Vector): real;
/// Вычисляет индекс Дэвиса–Булдина (Davies–Bouldin Index).
/// Чем меньше значение — тем лучше разделены кластеры.
/// Требует минимум 2 кластера.
/// Сложность O(n·p + k²·p).
static function DaviesBouldinScore(X: Matrix; labels: Vector): real;
/// Adjusted Rand Index (ARI).
/// Внешняя метрика качества кластеризации.
/// Сравнивает предсказанные кластеры с истинными метками.
///
/// Основана на анализе всех пар объектов:
/// проверяет, совпадает ли решение алгоритма и истинная разметка
/// в вопросе "находятся ли две точки в одном кластере".
///
/// Корректирует обычный Rand Index на случайные совпадения.
///
/// Диапазон значений:
/// 1 — идеальное совпадение кластеризации и истинных меток
/// 0 — случайное разбиение
/// <0 — хуже случайного
static function AdjustedRandIndex(yTrue, yPred: Vector): real;
end;
type
/// Матрица ошибок (Confusion Matrix) для бинарной классификации.
/// Хранит количество истинно положительных (TP),
/// истинно отрицательных (TN),
/// ложно положительных (FP)
/// и ложно отрицательных (FN) предсказаний.
/// истинно отрицательных (TN),
/// ложно положительных (FP)
/// и ложно отрицательных (FN) предсказаний.
/// Используется для анализа качества классификатора
/// и вычисления метрик Accuracy, Precision, Recall и F1
/// и вычисления метрик Accuracy, Precision, Recall и F1
ConfusionMatrix = class
private
fTP, fTN, fFP, fFN: integer;
public
/// Создаёт матрицу ошибок по истинным меткам yTrue
/// и предсказаниям модели yPred.
/// Ожидается, что векторы содержат значения 0 и 1
/// и имеют одинаковую длину.
/// Подсчитывает TP, TN, FP и FN.
/// Создаёт матрицу ошибок по истинным меткам yTrue и предсказаниям модели yPred.
/// Ожидается, что векторы содержат значения 0 и 1 и имеют одинаковую длину.
/// Подсчитывает TP, TN, FP и FN
constructor Create(yTrue, yPred: Vector);
/// Истинно положительные (True Positives).
/// Количество объектов класса 1,
/// которые модель правильно предсказала как 1.
/// Количество объектов класса 1, которые модель правильно предсказала как 1.
property TP: integer read fTP;
/// Истинно отрицательные (True Negatives).
/// Количество объектов класса 0,
/// которые модель правильно предсказала как 0.
/// Количество объектов класса 0, которые модель правильно предсказала как 0.
property TN: integer read fTN;
/// Ложно положительные (False Positives).
/// Количество объектов класса 0,
/// которые модель ошибочно предсказала как 1.
/// Количество объектов класса 0, которые модель ошибочно предсказала как 1.
property FP: integer read fFP;
/// Ложно отрицательные (False Negatives).
/// Количество объектов класса 1,
/// которые модель ошибочно предсказала как 0.
/// Количество объектов класса 1, которые модель ошибочно предсказала как 0.
property FN: integer read fFN;
/// Доля правильных предсказаний.
@ -139,12 +176,12 @@ type
function Accuracy: real;
/// Точность (Precision) для положительного класса (1).
/// Среди всех объектов, которые модель предсказала как 1,
/// показывает долю действительно принадлежащих этому классу.
/// показывает долю действительно принадлежащих этому классу.
/// Важна, когда критичны ложные положительные результаты («ложные тревоги»).
function Precision: real;
/// Полнота (Recall) для положительного класса (1).
/// Среди всех объектов, которые в действительности равны 1,
/// показывает долю правильно найденных моделью.
/// показывает долю правильно найденных моделью.
/// Важна, когда нежелательны пропуски положительных случаев (например, заболеваний).
function Recall: real;
/// F1-мера.
@ -167,8 +204,19 @@ const
ER_DIV_BY_ZERO =
'Деление на ноль в {0}!!Division by zero in {0}';
ER_INVALID_CLASS_LABEL =
'Некорректная метка класса: {0}!!Invalid class label: {0}';
'Некорректная метка класса: {0}!!Invalid class label: {0}';
ER_STRATIFIED_LABELS_INVALID =
'Метки кластеров должны быть целыми числами!!Cluster labels must be integers';
ER_SILHOUETTE_NEEDS_2_CLUSTERS =
'Для вычисления Silhouette требуется минимум 2 кластера!!Silhouette requires at least 2 clusters';
ER_INVALID_VALUE_AT =
'Некорректное значение в {0} на позиции {1}!!Invalid value in {0} at index {1}';
ER_CH_NEEDS_2_CLUSTERS =
'Для Calinski–Harabasz требуется минимум 2 кластера!!Calinski–Harabasz requires at least 2 clusters';
ER_CH_NEEDS_N_GT_K =
'Для Calinski–Harabasz требуется n > k!!Calinski–Harabasz requires n > k';
ER_LABELS_MUST_BE_INTEGERS =
'Метки кластеров должны быть целыми числами!!Cluster labels must be integers';
//-----------------------------
// Metrics
//-----------------------------
@ -494,6 +542,457 @@ begin
Result := ConfusionMatrix.Create(yTrue, yPred).BalancedAccuracy;
end;
static function Metrics.SilhouetteSamples(X: Matrix; labels: Vector): Vector;
begin
if X = nil then
ArgumentNullError(ER_ARG_NULL, 'X');
if labels = nil then
ArgumentNullError(ER_ARG_NULL, 'labels');
var n := X.RowCount;
var p := X.ColCount;
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'SilhouetteSamples');
if labels.Length <> n then
DimensionError(ER_DIM_MISMATCH, labels.Length, n);
// --- собираем размеры кластеров и маппинг label -> 0..c-1
var map := new Dictionary<integer, integer>();
var counts := new List<integer>();
var clsId := new integer[n];
for var i := 0 to n - 1 do
begin
var v := labels[i];
var lab := integer(v);
if Abs(v - lab) > 1e-12 then
ArgumentError(ER_LABELS_MUST_BE_INTEGERS);
var id: integer;
if map.TryGetValue(lab, id) then
begin
clsId[i] := id;
counts[id] += 1;
end
else
begin
id := map.Count;
map.Add(lab, id);
clsId[i] := id;
counts.Add(1);
end;
end;
var cCount := counts.Count;
if cCount < 2 then
ArgumentError(ER_SILHOUETTE_NEEDS_2_CLUSTERS);
// --- список уникальных кластеров, чтобы не обращаться к Dictionary в hot-path
var sizes := counts.ToArray;
// --- результат
Result := new Vector(n);
// --- рабочие массивы (переиспользуем, без аллокаций в цикле по i)
var sumToCluster := new real[cCount];
for var i := 0 to n - 1 do
begin
// обнуляем суммы
for var c := 0 to cCount - 1 do
sumToCluster[c] := 0.0;
// суммируем расстояния от i до всех остальных по кластерам
for var t := 0 to n - 1 do
if t <> i then
begin
var dist := 0.0;
for var j := 0 to p - 1 do
begin
var a := X[i,j];
if double.IsNaN(a) or double.IsInfinity(a) then
ArgumentError(ER_INVALID_VALUE_AT, 'X', i);
var b := X[t,j];
if double.IsNaN(b) or double.IsInfinity(b) then
ArgumentError(ER_INVALID_VALUE_AT, 'X', t);
var d := a - b;
dist += d * d;
end;
sumToCluster[clsId[t]] += Sqrt(dist);
end;
var my := clsId[i];
// a(i): среднее расстояние до своего кластера (без самой точки)
var aMean := 0.0;
if sizes[my] > 1 then
aMean := sumToCluster[my] / (sizes[my] - 1)
else
aMean := 0.0;
// b(i): минимальное среднее расстояние до другого кластера
var bMean := double.MaxValue;
for var c := 0 to cCount - 1 do
if c <> my then
begin
if sizes[c] = 0 then
continue;
var mean := sumToCluster[c] / sizes[c];
if mean < bMean then
bMean := mean;
end;
if double.IsInfinity(bMean) then
bMean := 0.0;
var denom := Max(aMean, bMean);
if denom = 0 then
Result[i] := 0.0
else
Result[i] := (bMean - aMean) / denom;
end;
end;
static function Metrics.SilhouetteScore(X: Matrix; labels: Vector): real;
begin
var s := SilhouetteSamples(X, labels);
var n := s.Length;
var sum := 0.0;
for var i := 0 to n - 1 do
sum += s[i];
if n = 0 then
exit(0.0);
Result := sum / n;
end;
/// Вычисляет индекс Калински–Харабаса (Calinski–Harabasz Index).
/// Возвращает отношение межкластерной дисперсии к внутрикластерной дисперсии.
/// Чем больше значение — тем лучше разделены кластеры.
/// При нулевой внутрикластерной дисперсии возвращает +∞.
/// Требует как минимум 2 кластера и n > k.
/// Сложность O(n·p).
static function Metrics.CalinskiHarabaszScore(X: Matrix; labels: Vector): real;
begin
if X = nil then
ArgumentNullError(ER_ARG_NULL, 'X');
if labels = nil then
ArgumentNullError(ER_ARG_NULL, 'labels');
var n := X.RowCount;
var p := X.ColCount;
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'CalinskiHarabaszScore');
if labels.Length <> n then
DimensionError(ER_DIM_MISMATCH, labels.Length, n);
// --- label -> 0..k-1 + sizes
var map := new Dictionary<integer, integer>();
var counts := new List<integer>();
var clsId := new integer[n];
for var i := 0 to n - 1 do
begin
var v := labels[i];
var lab := integer(v);
if Abs(v - lab) > 1e-12 then
ArgumentError(ER_LABELS_MUST_BE_INTEGERS);
var id: integer;
if map.TryGetValue(lab, id) then
begin
clsId[i] := id;
counts[id] += 1;
end
else
begin
id := map.Count;
map.Add(lab, id);
clsId[i] := id;
counts.Add(1);
end;
end;
var k := counts.Count;
if k < 2 then
ArgumentError(ER_CH_NEEDS_2_CLUSTERS);
if k >= n then
ArgumentError(ER_CH_NEEDS_N_GT_K);
var sizes := counts.ToArray;
// --- глобальное среднее
var globalMean := new real[p];
for var i := 0 to n - 1 do
for var j := 0 to p - 1 do
begin
var xij := X[i,j];
if double.IsNaN(xij) or double.IsInfinity(xij) then
ArgumentError(ER_INVALID_VALUE_AT, 'X', i);
globalMean[j] += xij;
end;
for var j := 0 to p - 1 do
globalMean[j] /= n;
// --- центры кластеров
var centers := new Matrix(k, p);
for var i := 0 to n - 1 do
begin
var c := clsId[i];
for var j := 0 to p - 1 do
centers[c,j] += X[i,j];
end;
for var c := 0 to k - 1 do
for var j := 0 to p - 1 do
centers[c,j] /= sizes[c];
// --- Within (W): сумма квадратов расстояний до центров кластеров
var W := 0.0;
for var i := 0 to n - 1 do
begin
var c := clsId[i];
var dist := 0.0;
for var j := 0 to p - 1 do
begin
var d := X[i,j] - centers[c,j];
dist += d * d;
end;
W += dist;
end;
// --- Between (B): сумма n_c * ||mu_c - mu||^2
var B := 0.0;
for var c := 0 to k - 1 do
begin
var dist := 0.0;
for var j := 0 to p - 1 do
begin
var d := centers[c,j] - globalMean[j];
dist += d * d;
end;
B += sizes[c] * dist;
end;
if W = 0 then
exit(double.PositiveInfinity);
var ch := (B / (k - 1)) / (W / (n - k));
if double.IsNaN(ch) or double.IsInfinity(ch) then
ArgumentError(ER_INVALID_VALUE, 'CalinskiHarabaszScore');
Result := ch;
end;
static function Metrics.DaviesBouldinScore(X: Matrix; labels: Vector): real;
begin
if X = nil then
ArgumentNullError(ER_ARG_NULL, 'X');
if labels = nil then
ArgumentNullError(ER_ARG_NULL, 'labels');
var n := X.RowCount;
var p := X.ColCount;
if labels.Length <> n then
DimensionError(ER_DIM_MISMATCH, labels.Length, n);
// --- label -> 0..k-1
var map := new Dictionary<integer, integer>();
var counts := new List<integer>();
var clsId := new integer[n];
for var i := 0 to n - 1 do
begin
var v := labels[i];
var lab := integer(v);
if Abs(v - lab) > 1e-12 then
ArgumentError(ER_LABELS_MUST_BE_INTEGERS);
var id: integer;
if map.TryGetValue(lab, id) then
begin
clsId[i] := id;
counts[id] += 1;
end
else
begin
id := map.Count;
map.Add(lab, id);
clsId[i] := id;
counts.Add(1);
end;
end;
var k := counts.Count;
if k < 2 then
ArgumentError(ER_CH_NEEDS_2_CLUSTERS);
var sizes := counts.ToArray;
// --- центры
var centers := new Matrix(k, p);
for var i := 0 to n - 1 do
begin
var c := clsId[i];
for var j := 0 to p - 1 do
centers[c,j] += X[i,j];
end;
for var c := 0 to k - 1 do
for var j := 0 to p - 1 do
centers[c,j] /= sizes[c];
// --- Si: среднее расстояние точек до центра
var S := new real[k];
for var i := 0 to n - 1 do
begin
var c := clsId[i];
var dist := 0.0;
for var j := 0 to p - 1 do
begin
var d := X[i,j] - centers[c,j];
dist += d * d;
end;
S[c] += Sqrt(dist);
end;
for var c := 0 to k - 1 do
S[c] /= sizes[c];
// --- считаем DB
var total := 0.0;
for var i := 0 to k - 1 do
begin
var maxR := 0.0;
for var j := 0 to k - 1 do
if i <> j then
begin
var dist := 0.0;
for var d := 0 to p - 1 do
begin
var delta := centers[i,d] - centers[j,d];
dist += delta * delta;
end;
dist := Sqrt(dist);
if dist = 0 then
continue;
var Rij := (S[i] + S[j]) / dist;
if Rij > maxR then
maxR := Rij;
end;
total += maxR;
end;
Result := total / k;
end;
/// Adjusted Rand Index (ARI).
/// Внешняя метрика качества кластеризации.
/// Сравнивает предсказанные кластеры с истинными метками.
///
/// Основана на анализе всех пар объектов:
/// проверяет, совпадает ли решение алгоритма и истинная разметка
/// в вопросе "находятся ли две точки в одном кластере".
///
/// Корректирует обычный Rand Index на случайные совпадения.
///
/// Диапазон значений:
/// 1 — идеальное совпадение кластеризации и истинных меток
/// 0 — случайное разбиение
/// <0 — хуже случайного
static function Metrics.AdjustedRandIndex(yTrue, yPred: Vector): real;
begin
if yTrue.Length <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length);
var n := yTrue.Length;
if n < 2 then
Result := 1
else
begin
var tp := 0;
var tn := 0;
var fp := 0;
var fn := 0;
for var i := 0 to n-2 do
for var j := i+1 to n-1 do
begin
var sameTrue := yTrue[i] = yTrue[j];
var samePred := yPred[i] = yPred[j];
if sameTrue and samePred then
tp += 1
else if (not sameTrue) and (not samePred) then
tn += 1
else if (not sameTrue) and samePred then
fp += 1
else
fn += 1;
end;
var total := tp + tn + fp + fn;
var ri := (tp + tn) / total;
var a := tp + fp;
var b := tp + fn;
var c := fn + tn;
var d := fp + tn;
var expected := ((a*b) + (c*d)) / (total*total);
if 1 - expected = 0 then
Result := 0
else
Result := (ri - expected) / (1 - expected);
end;
end;
//-----------------------------
// ConfusionMatrix
//-----------------------------

682
bin/Lib/PlotML.pas Normal file
View file

@ -0,0 +1,682 @@
unit PlotML;
{$reference %GAC%\InteractiveDataDisplay.WPF.dll}
{$reference 'PresentationFramework.dll'}
{$reference 'WindowsBase.dll'}
{$reference 'PresentationCore.dll'}
interface
uses System,
System.Windows,
System.Windows.Controls,
System.Windows.Media,
System.Threading,
System.Windows.Threading,
InteractiveDataDisplay.WPF;
type
ApplicationWPF = System.Windows.Application;
WindowWPF = System.Windows.Window;
GridWPF = System.Windows.Controls.Grid;
ChartWPF = InteractiveDataDisplay.WPF.Chart;
LineGraphWPF = InteractiveDataDisplay.WPF.LineGraph;
MarkerGraphWPF = InteractiveDataDisplay.WPF.CircleMarkerGraph;
BrushWPF = System.Windows.Media.SolidColorBrush;
Colors = System.Windows.Media.Colors;
ColorWPF = System.Windows.Media.Color;
MarkerType = (Circle, Box, Triangle, Diamond, Cross);
Palettes = static class
public
const &Default = 'default';
const Dark = 'dark';
const Pastel = 'pastel';
const Bright = 'bright';
const Muted = 'muted';
end;
Palette = class
public
Colors: array of Color;
constructor Create(params c: array of Color);
end;
Figure = class;
Cell = class
private
parentGrid: GridWPF;
row,col: integer;
chart: ChartWPF;
palette: Palette;
paletteIndex: integer := 0;
function NextColor: Color;
procedure EnsureChart;
public
constructor Create(g: GridWPF; r,c: integer);
procedure LineGraph(x, y: array of real; color: ColorWPF? := nil;
thickness: real := 2; legend: string := nil);
procedure Points(x, y: array of real; color: ColorWPF? := nil;
size: real := 6; marker: MarkerType := MarkerType.Circle; legend: string := nil);
procedure Heatmap(m: array[,] of real);
procedure SetPalette(name: string);
procedure Title(s: string);
procedure XLabel(s: string);
procedure YLabel(s: string);
procedure Limits(xmin, xmax, ymin, ymax: real);
procedure XLim(xmin,xmax: real);
procedure YLim(ymin,ymax: real);
procedure Clear;
end;
Figure = class
private
grid: GridWPF;
cells: array[,] of Cell;
public
constructor Create(rows,cols: integer);
property Item[r,c: integer]: Cell read cells[r,c]; default;
end;
Plot = static class
private
static procedure RunUI(a: procedure);
static function CreateLineSeries(x,y: array of real; c: Color): LineGraphWPF;
static function CreatePointSeries(x, y: array of real;
color: ColorWPF; size: real; marker: MarkerType): MarkerGraphWPF;
static procedure DrawLine(chart: ChartWPF; x, y: array of real;
color: ColorWPF; thickness: real; legend: string);
static procedure DrawPoints(chart: ChartWPF; x, y: array of real;
color: ColorWPF; size: real; marker: MarkerType; legend: string);
static procedure DrawHeatmap(chart: ChartWPF; m: array[,] of real);
public
static procedure AddSeries(chart: ChartWPF; series: UIElement);
static procedure LineGraph(x, y: array of real;
color: ColorWPF? := nil; thickness: real := 2; legend: string := nil);
static procedure Points(x, y: array of real;
color: ColorWPF? := nil; size: real := 6; marker: MarkerType := MarkerType.Circle; legend: string := nil);
static procedure Heatmap(m: array[,] of real);
static function Grid(rows,cols: integer): Figure;
static procedure SetPalette(name: string);
static procedure Limits(xmin,xmax,ymin,ymax: real);
static procedure XLim(xmin,xmax: real);
static procedure YLim(ymin,ymax: real);
static procedure Clear;
static procedure Save(filename: string);
end;
implementation
var
uiThread: Thread;
uiDispatcher: Dispatcher;
app: ApplicationWPF;
win: WindowWPF;
rootChart: ChartWPF;
paletteDict: Dictionary<string,Palette>;
currentPalette: Palette;
rootPaletteIndex: integer := 0;
function CreateMarker(t: MarkerType): InteractiveDataDisplay.WPF.ColorSizeMarker;
begin
case t of
MarkerType.Circle: Result := new CircleMarker;
MarkerType.Box: Result := new BoxMarker;
MarkerType.Triangle: Result := new TriangleMarker;
MarkerType.Diamond: Result := new DiamondMarker;
MarkerType.Cross: Result := new CrossMarker;
end;
end;
function NextRootColor: ColorWPF;
begin
var c := currentPalette.Colors[
rootPaletteIndex mod currentPalette.Colors.Length
];
rootPaletteIndex += 1;
Result := c;
end;
function MakeHistogram(data: array of real; bins: integer): (array of real, array of real);
begin
var xmin := data.Min;
var xmax := data.Max;
var h := (xmax-xmin)/bins;
var counts := new real[bins];
foreach var v in data do
begin
var k := integer((v-xmin)/h);
if k=bins then k := bins-1;
counts[k] += 1;
end;
var xs := ArrGen(bins, i -> xmin + (i+0.5)*h);
Result := (xs,counts);
end;
procedure InitPalettes;
begin
paletteDict := new Dictionary<string,Palette>;
paletteDict['default'] := new Palette(
Color.FromRgb($1f,$77,$b4),
Color.FromRgb($ff,$7f,$0e),
Color.FromRgb($2c,$a0,$2c),
Color.FromRgb($d6,$27,$28),
Color.FromRgb($94,$67,$bd),
Color.FromRgb($8c,$56,$4b),
Color.FromRgb($e3,$77,$c2),
Color.FromRgb($7f,$7f,$7f),
Color.FromRgb($bc,$bd,$22),
Color.FromRgb($17,$be,$cf)
);
paletteDict['pastel'] := new Palette(
Color.FromRgb(141,211,199),
Color.FromRgb(255,255,179),
Color.FromRgb(190,186,218),
Color.FromRgb(251,128,114),
Color.FromRgb(128,177,211),
Color.FromRgb(253,180,98),
Color.FromRgb(179,222,105),
Color.FromRgb(252,205,229)
);
paletteDict['dark'] := new Palette(
Color.FromRgb(27,158,119),
Color.FromRgb(217,95,2),
Color.FromRgb(117,112,179),
Color.FromRgb(231,41,138),
Color.FromRgb(102,166,30),
Color.FromRgb(230,171,2),
Color.FromRgb(166,118,29),
Color.FromRgb(102,102,102)
);
paletteDict['bright'] := new Palette(
Color.FromRgb(0,114,178),
Color.FromRgb(230,159,0),
Color.FromRgb(0,158,115),
Color.FromRgb(213,94,0),
Color.FromRgb(204,121,167),
Color.FromRgb(86,180,233)
);
paletteDict['muted'] := new Palette(
Color.FromRgb(76,114,176),
Color.FromRgb(221,132,82),
Color.FromRgb(85,168,104),
Color.FromRgb(196,78,82),
Color.FromRgb(129,114,179),
Color.FromRgb(147,120,96)
);
currentPalette := paletteDict['default'];
end;
procedure InitUI;
begin
uiThread := new Thread(() ->
begin
app := new ApplicationWPF;
app.Dispatcher.UnhandledException += (o,e) ->
begin
Println(e.Exception.Message);
if e.Exception.InnerException <> nil then
Println(e.Exception.InnerException.Message);
halt;
end;
rootChart := new ChartWPF;
win := new WindowWPF;
win.Title := 'PlotML';
win.Width := 800;
win.Height := 600;
win.Content := rootChart;
win.Closed += (o,e) ->
Dispatcher.CurrentDispatcher.BeginInvokeShutdown(
DispatcherPriority.Normal);
uiDispatcher := Dispatcher.CurrentDispatcher;
InitPalettes;
win.Show;
Dispatcher.Run;
end);
uiThread.SetApartmentState(ApartmentState.STA);
uiThread.Start;
while uiDispatcher = nil do
Sleep(10);
end;
constructor Palette.Create(params c: array of Color);
begin
Colors := c;
end;
constructor Cell.Create(g: GridWPF; r,c: integer);
begin
parentGrid := g;
row := r;
col := c;
palette := currentPalette;
end;
function Cell.NextColor: ColorWPF;
begin
var c := palette.Colors[
paletteIndex mod palette.Colors.Length
];
paletteIndex += 1;
Result := c;
end;
procedure Cell.SetPalette(name: string);
begin
if paletteDict.ContainsKey(name) then
begin
palette := paletteDict[name];
paletteIndex := 0;
end;
end;
procedure Cell.Title(s: string);
begin
Plot.RunUI(() ->
begin
EnsureChart;
chart.Title := s;
end);
end;
procedure Cell.XLabel(s: string);
begin
Plot.RunUI(() ->
begin
EnsureChart;
chart.BottomTitle := s;
end);
end;
procedure Cell.YLabel(s: string);
begin
Plot.RunUI(() ->
begin
EnsureChart;
chart.LeftTitle := s;
end);
end;
procedure Cell.Limits(xmin,xmax,ymin,ymax: real);
begin
Plot.RunUI(() ->
begin
EnsureChart;
chart.PlotOriginX := xmin;
chart.PlotOriginY := ymin;
chart.PlotWidth := xmax - xmin;
chart.PlotHeight := ymax - ymin;
end);
end;
procedure Cell.XLim(xmin,xmax: real);
begin
Plot.RunUI(() ->
begin
EnsureChart;
chart.PlotOriginX := xmin;
chart.PlotWidth := xmax - xmin;
end);
end;
procedure Cell.YLim(ymin,ymax: real);
begin
Plot.RunUI(() ->
begin
EnsureChart;
chart.PlotOriginY := ymin;
chart.PlotHeight := ymax - ymin;
end);
end;
procedure Cell.Clear;
begin
Plot.RunUI(() ->
begin
if chart = nil then exit;
var container := chart.Content as GridWPF;
if container <> nil then
container.Children.Clear;
paletteIndex := 0;
end);
end;
procedure Cell.EnsureChart;
begin
if chart <> nil then exit;
chart := new ChartWPF;
chart.LegendVisibility := Visibility.Hidden;
var container := new GridWPF;
chart.Content := container;
GridWPF.SetRow(chart,row);
GridWPF.SetColumn(chart,col);
parentGrid.Children.Add(chart);
end;
procedure Cell.LineGraph(x, y: array of real; color: ColorWPF?;
thickness: real; legend: string);
begin
Plot.RunUI(() ->
begin
EnsureChart;
var clr := if color.HasValue then color.Value else NextColor;
Plot.DrawLine(chart, x, y, clr, thickness, legend);
end);
end;
procedure Cell.Points(x, y: array of real; color: ColorWPF?;
size: real; marker: MarkerType; legend: string);
begin
Plot.RunUI(() ->
begin
EnsureChart;
var clr := if color.HasValue then color.Value else NextColor;
Plot.DrawPoints(chart, x, y, clr, size, marker, legend);
end);
end;
procedure Cell.Heatmap(m: array[,] of real);
begin
Plot.RunUI(() ->
begin
EnsureChart;
Plot.DrawHeatmap(chart, m);
end);
end;
constructor Figure.Create(rows,cols: integer);
begin
grid := new GridWPF;
for var i:=0 to rows-1 do
grid.RowDefinitions.Add(new RowDefinition);
for var j:=0 to cols-1 do
grid.ColumnDefinitions.Add(new ColumnDefinition);
cells := new Cell[rows,cols];
for var i:=0 to rows-1 do
for var j:=0 to cols-1 do
cells[i,j] := new Cell(grid,i,j);
end;
static procedure Plot.RunUI(a: procedure);
begin
if Dispatcher.CurrentDispatcher = uiDispatcher then
a()
else
uiDispatcher.Invoke(a);
end;
static procedure Plot.Limits(xmin,xmax,ymin,ymax: real);
begin
RunUI(() ->
begin
rootChart.PlotOriginX := xmin;
rootChart.PlotOriginY := ymin;
rootChart.PlotWidth := xmax - xmin;
rootChart.PlotHeight := ymax - ymin;
end);
end;
static procedure Plot.XLim(xmin,xmax: real);
begin
RunUI(() ->
begin
rootChart.PlotOriginX := xmin;
rootChart.PlotWidth := xmax - xmin;
end);
end;
static procedure Plot.YLim(ymin,ymax: real);
begin
RunUI(() ->
begin
rootChart.PlotOriginY := ymin;
rootChart.PlotHeight := ymax - ymin;
end);
end;
static procedure Plot.Clear;
begin
RunUI(() ->
begin
var container := rootChart.Content as GridWPF;
if container <> nil then
container.Children.Clear;
end);
end;
static procedure Plot.Save(filename: string);
begin
RunUI(() ->
begin
var rtb := new System.Windows.Media.Imaging.RenderTargetBitmap(
integer(win.Width),
integer(win.Height),
96, 96,
PixelFormats.Pbgra32
);
rtb.Render(win);
var encoder := new System.Windows.Media.Imaging.PngBitmapEncoder;
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(rtb));
var fs := new System.IO.FileStream(filename, System.IO.FileMode.Create);
encoder.Save(fs);
fs.Close;
end);
end;
static function Plot.CreateLineSeries(x,y: array of real; c: Color): LineGraphWPF;
begin
var g := new LineGraphWPF;
g.Stroke := new BrushWPF(c);
g.StrokeThickness := 2;
g.Plot(x,y);
Result := g;
end;
static function Plot.CreatePointSeries(x, y: array of real;
color: ColorWPF; size: real; marker: MarkerType): MarkerGraphWPF;
begin
var g := new MarkerGraphWPF;
{var alpha := 0.5;
var a := round(alpha * 255);
var c := ColorWPF.FromArgb(a, color.R, color.G, color.B);
g.Color := c;}
g.Color := color;
g.Size := size;
g.MarkerType := CreateMarker(marker);
g.StrokeThickness := 0;
g.Plot(x,y);
Result := g;
end;
static procedure Plot.DrawLine(chart: ChartWPF; x, y: array of real;
color: ColorWPF; thickness: real; legend: string);
begin
var g := CreateLineSeries(x, y, color);
g.StrokeThickness := thickness;
if legend <> nil then
begin
g.Description := legend;
chart.LegendVisibility := Visibility.Visible;
end;
AddSeries(chart, g);
end;
static procedure Plot.DrawPoints(chart: ChartWPF; x, y: array of real;
color: ColorWPF; size: real; marker: MarkerType; legend: string);
begin
var g := CreatePointSeries(x, y, color, size, marker);
if legend <> nil then
begin
g.Description := legend;
chart.LegendVisibility := Visibility.Visible;
end;
AddSeries(chart, g);
end;
static procedure Plot.DrawHeatmap(chart: ChartWPF; m: array[,] of real);
begin
var rows := m.GetLength(0);
var cols := m.GetLength(1);
var x := ArrGen(cols, i -> i);
var y := ArrGen(rows, i -> i);
var g := new HeatmapGraph;
g.Plot(m, x, y);
AddSeries(chart, g);
end;
class procedure Plot.AddSeries(chart: ChartWPF; series: UIElement);
begin
var container := chart.Content as GridWPF;
if container = nil then
begin
container := new GridWPF;
chart.Content := container;
end;
container.Children.Add(series);
end;
class procedure Plot.LineGraph(x, y: array of real;
color: ColorWPF?; thickness: real; legend: string);
begin
RunUI(() ->
begin
var clr := if color.HasValue then color.Value else NextRootColor;
DrawLine(rootChart, x, y, clr, thickness, legend);
end);
end;
static procedure Plot.Points(x, y: array of real;
color: ColorWPF?; size: real; marker: MarkerType; legend: string);
begin
RunUI(() ->
begin
var clr := if color.HasValue then color.Value else NextRootColor;
DrawPoints(rootChart, x, y, clr, size, marker, legend);
end);
end;
static procedure Plot.Heatmap(m: array[,] of real);
begin
RunUI(() ->
begin
DrawHeatmap(rootChart, m);
end);
end;
static function Plot.Grid(rows,cols: integer): Figure;
begin
var fig: Figure;
RunUI(() ->
begin
fig := new Figure(rows,cols);
win.Content := fig.grid;
end);
Result := fig;
end;
static procedure Plot.SetPalette(name: string);
begin
RunUI(() ->
begin
if name in paletteDict then
currentPalette := paletteDict[name];
end);
end;
initialization
InitUI;
end.

View file

@ -40,11 +40,11 @@ type
/// Кодирует строковый категориальный столбец в числовые значения
/// Категории фиксируются при Fit
/// Работает только со строковыми столбцами
LabelEncoder = class(IPreprocessor)
private
col: string;
mapping: Dictionary<string, integer>;
fitted: boolean;
LabelEncoder = class(IPreprocessor)
private
col: string;
mapping: Dictionary<string, integer>;
fitted: boolean;
public
/// Создаёт LabelEncoder для указанного столбца
constructor Create(column: string);
@ -56,11 +56,13 @@ type
function Transform(df: DataFrame): DataFrame;
/// Выполняет Fit и Transform последовательно
function FitTransform(df: DataFrame): DataFrame;
function ToString: string; override;
end;
/// Кодирует строковый категориальный столбец в набор бинарных (one-hot) столбцов
/// Категории фиксируются при Fit
/// Неизвестные категории приводят к ошибке
/// Неизвестные категории кодируются нулями
/// Пропущенные значения (NA) кодируются нулями
OneHotEncoder = class(IPreprocessor)
private
@ -79,6 +81,8 @@ type
function Transform(df: DataFrame): DataFrame;
/// Выполняет Fit и Transform последовательно
function FitTransform(df: DataFrame): DataFrame;
function ToString: string; override;
end;
ImputeStrategy = (isMean, isConstant);
@ -94,10 +98,12 @@ type
means: array of real;
fitted: boolean;
public
/// Создаёт Imputer с заполнением средним значением
constructor Create(params columns: array of string);
/// Создаёт Imputer с заданной стратегией заполнения
constructor Create(strategy: ImputeStrategy; params columns: array of string);
/// Создаёт Imputer с константной стратегией заполнения
constructor Create(strategy: ImputeStrategy; value: object; params columns: array of string);
constructor Create(value: object; params columns: array of string);
/// Вычисляет значения для заполнения пропусков
function Fit(df: DataFrame): IPreprocessor;
@ -106,6 +112,8 @@ type
function Transform(df: DataFrame): DataFrame;
/// Выполняет Fit и Transform последовательно
function FitTransform(df: DataFrame): DataFrame;
function ToString: string; override;
end;
@ -171,193 +179,11 @@ const
ER_IMPUTER_CONSTANT_TYPE_MISMATCH =
'Imputer(constant): несоответствие типа значения для столбца "{0}"!!' +
'Imputer(constant): value type mismatch for column "{0}"';
ER_ONEHOT_EMPTY_COLUMN =
'Столбец "{0}" не содержит категориальных значений!!Column "{0}" contains no categorical values';
ER_COLUMN_NOT_FOUND =
'Столбец "{0}" не найден!!Column "{0}" not found';
//-----------------------------
// StandardScaler
//-----------------------------
{constructor DataStandardScaler.Create(params columns: array of string);
begin
if (columns = nil) or (columns.Length = 0) then
ArgumentError(ER_SCALER_NO_COLUMNS);
cols := columns;
fitted := false;
end;
function DataStandardScaler.Fit(df: DataFrame): IPreprocessor;
begin
var n := cols.Length;
SetLength(means, n);
SetLength(stds, n);
for var i := 0 to n - 1 do
begin
var colName := cols[i];
var idx := df.Schema.IndexOf(colName);
var ct := df.Schema.ColumnTypeAt(idx);
if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then
Error(ER_SCALER_COLUMN_NOT_NUMERIC, colName);
var sum := 0.0;
var sum2 := 0.0;
var cnt := 0;
var cur := df.GetCursor;
while cur.MoveNext do
begin
if not cur.IsValid(idx) then continue;
var x := cur.Float(idx);
sum += x;
sum2 += x * x;
cnt += 1;
end;
if cnt = 0 then
Error(ER_SCALER_NO_VALID_VALUES, colName);
means[i] := sum / cnt;
var v := sum2 / cnt - means[i] * means[i];
stds[i] := Sqrt(v);
if stds[i] = 0 then
Error(ER_SCALER_ZERO_VARIANCE, colName);
end;
fitted := true;
Result := Self;
end;
function DataStandardScaler.Transform(df: DataFrame): DataFrame;
begin
if not fitted then
NotFittedError(ER_FIT_NOT_CALLED);
var res := df;
for var i := 0 to cols.Length - 1 do
begin
var colName := cols[i];
var idx := df.Schema.IndexOf(colName);
var mean := means[i];
var std := stds[i];
res := res.ReplaceColumnFloat(
colName,
c ->
(if c.IsValid(idx)
then (c.Float(idx) - mean) / std
else real.NaN)
);
end;
Result := res;
end;
function DataStandardScaler.FitTransform(df: DataFrame): DataFrame;
begin
Fit(df);
Result := Transform(df);
end;
//-----------------------------
// MinMaxScaler
//-----------------------------
constructor DataMinMaxScaler.Create(params columns: array of string);
begin
if (columns = nil) or (columns.Length = 0) then
ArgumentError(ER_MINMAX_NO_COLUMNS);
cols := columns;
fitted := false;
end;
function DataMinMaxScaler.Fit(df: DataFrame): IPreprocessor;
begin
var n := cols.Length;
SetLength(mins, n);
SetLength(maxs, n);
for var i := 0 to n - 1 do
begin
var colName := cols[i];
var idx := df.Schema.IndexOf(colName);
var ct := df.Schema.ColumnTypeAt(idx);
if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then
Error(ER_MINMAX_COLUMN_NOT_NUMERIC, colName);
var first := true;
var minv, maxv: real;
var cur := df.GetCursor;
while cur.MoveNext do
begin
if not cur.IsValid(idx) then continue;
var x := cur.Float(idx);
if first then
begin
minv := x;
maxv := x;
first := false;
end
else
begin
if x < minv then minv := x;
if x > maxv then maxv := x;
end;
end;
if first then
Error(ER_MINMAX_NO_VALID_VALUES, colName);
if minv = maxv then
Error(ER_MINMAX_CONSTANT_COLUMN, colName);
mins[i] := minv;
maxs[i] := maxv;
end;
fitted := true;
Result := Self;
end;
function DataMinMaxScaler.Transform(df: DataFrame): DataFrame;
begin
if not fitted then
NotFittedError(ER_FIT_NOT_CALLED);
var res := df;
for var i := 0 to cols.Length - 1 do
begin
var colName := cols[i];
var idx := df.Schema.IndexOf(colName);
var minv := mins[i];
var maxv := maxs[i];
var scale := maxv - minv;
res := res.ReplaceColumnFloat(
colName,
c ->
(if c.IsValid(idx)
then (c.Float(idx) - minv) / scale
else real.NaN)
);
end;
Result := res;
end;
function DataMinMaxScaler.FitTransform(df: DataFrame): DataFrame;
begin
Fit(df);
Result := Transform(df);
end;}
//-----------------------------
// LabelEncoder
//-----------------------------
@ -427,6 +253,11 @@ begin
Result := Transform(df);
end;
function LabelEncoder.ToString: string;
begin
Result := 'LabelEncoder(' + col + ')';
end;
//-----------------------------
// OneHotEncoder
//-----------------------------
@ -459,6 +290,9 @@ begin
values.Add(s);
end;
end;
if values.Count = 0 then
Error(ER_ONEHOT_EMPTY_COLUMN, col);
categories := values.ToArray;
fitted := true;
@ -471,22 +305,14 @@ begin
NotFittedError(ER_FIT_NOT_CALLED);
var srcIdx := df.Schema.IndexOf(col);
if srcIdx < 0 then
ArgumentError(ER_COLUMN_NOT_FOUND, col);
var catCount := categories.Length;
// === ШАГ 1. Проверка на unseen категории ===
var cur := df.GetCursor;
while cur.MoveNext do
begin
if not cur.IsValid(srcIdx) then continue;
var s := cur.Str(srcIdx);
if not indexByValue.ContainsKey(s) then
Error(ER_ONEHOT_UNSEEN_CATEGORY, s);
end;
// === ШАГ 2. Генерация one-hot столбцов ===
var res := df;
// === Генерация one-hot столбцов ===
for var j := 0 to catCount - 1 do
begin
var catIdx := j;
@ -500,7 +326,17 @@ begin
exit;
end;
if indexByValue[c.Str(srcIdx)] = catIdx then
var s := c.Str(srcIdx);
var idx: integer;
if not indexByValue.TryGetValue(s, idx) then
begin
// неизвестная категория → все нули
Result := 0;
exit;
end;
if idx = catIdx then
Result := 1
else
Result := 0;
@ -509,20 +345,23 @@ begin
res := res.AddDerivedIntColumn(newName, Encode);
end;
// === ШАГ 3. Удаление исходного столбца ===
// === удаление исходного столбца ===
res := res.Drop([srcIdx]);
Result := res;
end;
function OneHotEncoder.FitTransform(df: DataFrame): DataFrame;
begin
Fit(df);
Result := Transform(df);
end;
function OneHotEncoder.ToString: string;
begin
Result := 'OneHotEncoder(column=' + col + ')';
end;
//-----------------------------
// Imputer
//-----------------------------
@ -541,15 +380,17 @@ begin
fitted := false;
end;
constructor Imputer.Create(strategy: ImputeStrategy; value: object; params columns: array of string);
constructor Imputer.Create(params columns: array of string);
begin
if strategy <> isConstant then
ArgumentError(ER_IMPUTER_INVALID_STRATEGY_CONSTANT);
Create(ImputeStrategy.isMean, columns);
end;
constructor Imputer.Create(value: object; params columns: array of string);
begin
if (columns = nil) or (columns.Length = 0) then
ArgumentError(ER_IMPUTER_NO_COLUMNS);
self.strategy := strategy;
self.strategy := ImputeStrategy.isConstant;
self.cols := columns;
// одна и та же константа для всех столбцов
@ -669,5 +510,18 @@ begin
Result := Transform(df);
end;
function Imputer.ToString: string;
begin
var colsStr := '[' + cols.JoinToString(', ') + ']';
case strategy of
isMean:
Result := 'Imputer(strategy=mean, columns=' + colsStr + ')';
isConstant:
Result := 'Imputer(strategy=constant, value=' +
constants[0].ToString + ', columns=' + colsStr + ')';
end;
end;
end.