used_cars

Мелкие дополнения ML
This commit is contained in:
Mikhalkovich Stanislav 2026-07-01 20:40:56 +03:00
parent 7c31caa0b7
commit 911142c83e
14 changed files with 11221 additions and 16 deletions

View file

@ -22,7 +22,7 @@ begin
var y := df.EncodeLabels(ds.Target);
// Делим данные на обучающую и тестовую выборки
var (Xtrain, Xtest, ytrain, ytest) := Validation.TrainTestSplit(X, y, 0.2, 3);
var (Xtrain, Xtest, ytrain, ytest) := Validation.TrainTestSplit(X, y, 0.2, seed := 3);
// Создаём модель логистической регрессии
var model := new LogisticRegression(0.001);

View file

@ -0,0 +1,50 @@
uses MLABC,PlotML;
begin
var df := DataFrame.FromCsvText('''
Вес,ВысотаВХолке,Порода
20,33,бульдог
22,34,бульдог
24,35,бульдог
25,36,бульдог
19,35,бульдог
21,35,бульдог
26,34,бульдог
20,36,бульдог
14,39,спаниель
15,40,спаниель
16,41,спаниель
18,42,спаниель
17,43,спаниель
15,38,спаниель
19,40,спаниель
16,40,спаниель
''');
var X := df.ToMatrix(['Вес', 'ВысотаВХолке']);
var target := df.EncodeTarget('Порода');
var y := target.Labels;
var classNames := target.ClassNames;
var model := new LogisticRegression;
model.Fit(X, y);
var example := [20.0, 38.0];
var pred := model.PredictOne(example);
var prob := model.PredictProbaOne(example);
Println('Предсказанная порода:', classNames[pred]);
Println('Предсказанные вероятности:');
Println(classnames[0]:10,'-',prob[0]:0:2);
Println(classnames[1]:10,'-',prob[1]:0:2);
Plot.XLabel := df.ColumnNames[0];
Plot.YLabel := df.ColumnNames[1];
Plot.Title := 'Бульдоги и спаниели';
Plot.Points(X.Col(0),X.Col(1),y);
Plot.Points([example[0]],[example[1]],Colors.Black,size := 8);
var (A,B,C) := model.DecisionBoundary.Coefficients;
Plot.Line(A,B,C);
end.

View file

@ -496,7 +496,7 @@ namespace PascalABCCompiler.ParserTools
ss[0] = ss[0].Trim();
if (ss[0].Length > 0)
// мы сюда точно не должны попасть. Это должна быть обычная строка!
this.AddErrorFromResource("IMPOSSIBLE_MULTILINE_ERROR", sc);
this.AddErrorFromResource("IMPOSSIBLE_MULTILINE_ERROR"+" " + ss[0].Length.ToString(), sc);
if (ss[ss.Length - 1].Trim().Length > 0)
// There should be no non-whitespace characters before the closing quotes of the multiline string

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,45 @@
name = UsedCarsPrice
task = regression
features = model,year,transmission,mileage_km,fuelType,l_100km,engineSize,Make
target = price_k_rub
description.en = Used car prices with model, year, mileage, engine, fuel and transmission features. The target variable is price in thousand rubles.
description.ru = Цены на автомобили с пробегом по модели, году выпуска, пробегу, двигателю, топливу и типу коробки передач. Целевая переменная — цена в тыс. руб.
modified.en = Modified version with mileage, fuel consumption and price converted to units familiar to Russian users.
modified.ru = Модифицированная версия датасета с переводом пробега, расхода топлива и цены в привычные для российской аудитории величины.
source = Kaggle: Used Car Price Prediction Dataset
url = https://www.kaggle.com/datasets/taeefnajib/used-car-price-prediction-dataset
feature.model = categorical
feature.year = int
feature.price_k_rub = float
feature.transmission = categorical
feature.mileage_km = int
feature.fuelType = categorical
feature.l_100km = float
feature.engineSize = float
feature.Make = categorical
feature.model.ru = модель
feature.year.ru = год выпуска
feature.price_k_rub.ru = цена в тыс. руб.
feature.transmission.ru = коробка передач
feature.mileage_km.ru = пробег в километрах
feature.fuelType.ru = тип топлива
feature.l_100km.ru = расход в литрах на 100 км
feature.engineSize.ru = объём двигателя в литрах
feature.Make.ru = марка
feature.model.en = model
feature.year.en = year
feature.price_k_rub.en = price in thousand rubles
feature.transmission.en = transmission
feature.mileage_km.en = mileage in kilometres
feature.fuelType.en = fuel type
feature.l_100km.en = fuel consumption in litres per 100 km
feature.engineSize.en = engine size in litres
feature.Make.en = make

View file

@ -2,7 +2,19 @@
interface
// Здесь пока только методы расширения поэтому секция interface - пуста
type
/// Результат кодирования целевого столбца:
/// целочисленные метки и соответствующие имена классов.
EncodedTarget = record
Labels: array of integer;
ClassNames: array of string;
/// Возвращает код класса для строки с индексом rowIndex.
function ClassIndex(rowIndex: integer): integer;
/// Возвращает имя класса для строки с индексом rowIndex.
function ClassName(rowIndex: integer): string;
end;
implementation
@ -24,6 +36,28 @@ const
'Неподдерживаемый тип столбца "{0}" для EncodeLabels!!Unsupported column type "{0}" for EncodeLabels';
ER_TARGET_HAS_NA =
'Целевой столбец "{0}" содержит NA!!Target column "{0}" contains NA values';
ER_ENCODED_TARGET_ROW_INDEX_OUT_OF_RANGE =
'Индекс строки {0} вне диапазона [0, {1})!!Row index {0} is out of range [0, {1})';
ER_ENCODED_TARGET_CLASS_INDEX_OUT_OF_RANGE =
'Индекс класса {0} вне диапазона [0, {1})!!Class index {0} is out of range [0, {1})';
function EncodedTarget.ClassIndex(rowIndex: integer): integer;
begin
if (rowIndex < 0) or (rowIndex >= Labels.Length) then
ArgumentOutOfRangeError(ER_ENCODED_TARGET_ROW_INDEX_OUT_OF_RANGE, rowIndex, Labels.Length);
Result := Labels[rowIndex];
end;
function EncodedTarget.ClassName(rowIndex: integer): string;
begin
var cls := ClassIndex(rowIndex);
if (cls < 0) or (cls >= ClassNames.Length) then
ArgumentOutOfRangeError(ER_ENCODED_TARGET_CLASS_INDEX_OUT_OF_RANGE, cls, ClassNames.Length);
Result := ClassNames[cls];
end;
function ToMatrix(Self: DataFrame; colNames: array of string): Matrix; extensionmethod;
begin
@ -215,6 +249,16 @@ begin
end;
end;
/// Кодирует целевой столбец DataFrame и возвращает
/// целочисленные метки вместе с массивом имён классов.
/// Порядок кодирования соответствует порядку первого появления значений.
function EncodeTarget(Self: DataFrame; target: string): EncodedTarget; extensionmethod;
begin
var classes: array of string;
Result.Labels := Self.EncodeLabels(target, classes);
Result.ClassNames := classes;
end;
/// Преобразует строковые метки целевого столбца в целочисленные индексы (0,1,2,...)
/// с использованием заданного массива classes (mapping индекс метка).
/// classes должен быть получен ранее с помощью EncodeLabels.

View file

@ -99,11 +99,14 @@ type
function GetColumn(name: string): Column;
function GetSchema: DataFrameSchema;
procedure CheckRowIndex(rowIndex: integer);
function CloneWithSharedColumns: DataFrame;
public
/// Создает пустой DataFrame
constructor Create;
/// Возвращает имена столбцов
property ColumnNames: array of string read Schema.ColumnNames;
/// Возвращает копию схемы DataFrame
property Schema: DataFrameSchema read GetSchema;
@ -220,14 +223,34 @@ type
/// Возвращает целочисленный массив значений столбца с данным именем.
function Int(name: string): array of integer;
/// Возвращает целочисленное значение ячейки по индексам строки и столбца.
function Int(rowIndex, colIndex: integer): integer;
/// Возвращает целочисленное значение ячейки по индексу строки и имени столбца.
function Int(rowIndex: integer; colName: string): integer;
/// Возвращает вещественный массив значений столбца с данным именем.
function Float(name: string): array of real;
/// Возвращает вещественное значение ячейки по индексам строки и столбца.
function Float(rowIndex, colIndex: integer): real;
/// Возвращает вещественное значение ячейки по индексу строки и имени столбца.
function Float(rowIndex: integer; colName: string): real;
/// Возвращает строковый массив значений столбца с данным именем.
function Str(name: string): array of string;
/// Возвращает строковое значение ячейки по индексам строки и столбца.
function Str(rowIndex, colIndex: integer): string;
/// Возвращает строковое значение ячейки по индексу строки и имени столбца.
function Str(rowIndex: integer; colName: string): string;
/// Возвращает логический массив значений столбца с данным именем.
function Bool(name: string): array of boolean;
/// Возвращает логическое значение ячейки по индексам строки и столбца.
function Bool(rowIndex, colIndex: integer): boolean;
/// Возвращает логическое значение ячейки по индексу строки и имени столбца.
function Bool(rowIndex: integer; colName: string): boolean;
/// Возвращает массив DateTime-значений столбца с данным именем.
function DateTime(name: string): array of System.DateTime;
/// Возвращает DateTime-значение ячейки по индексам строки и столбца.
function DateTime(rowIndex, colIndex: integer): System.DateTime;
/// Возвращает DateTime-значение ячейки по индексу строки и имени столбца.
function DateTime(rowIndex: integer; colName: string): System.DateTime;
/// Вычисляет сумму значений столбца по индексу
function Sum(colIndex: integer): real;
@ -770,6 +793,8 @@ const
'Дублирующееся имя агрегированной колонки!!Duplicate aggregated column name';
ER_ROW_INDEX_OUT_OF_RANGE =
'Индекс строки вне диапазона!!Row index is out of range';
ER_VALUE_IS_NA =
'Значение в столбце "{0}" равно NA!!Value in column "{0}" is NA';
ER_UNSUPPORTED_COLUMN_TYPE =
'Неподдерживаемый тип столбца!!Unsupported column type';
ER_JOIN_FLOAT_KEY_NOT_SUPPORTED =
@ -2473,6 +2498,12 @@ begin
);
end;
procedure DataFrame.CheckRowIndex(rowIndex: integer);
begin
if (rowIndex < 0) or (rowIndex >= RowCount) then
ArgumentOutOfRangeError(ER_ROW_INDEX_OUT_OF_RANGE);
end;
function DataFrame.GetCursor: DataFrameCursor :=
new DataFrameCursor(columns.ToArray,fSchema);
@ -2516,26 +2547,106 @@ begin
Result := GetIntColumn(name);
end;
function DataFrame.Int(rowIndex, colIndex: integer): integer;
begin
CheckRowIndex(rowIndex);
CheckColumnIndex(colIndex);
var c := IntColumn(columns[colIndex]);
if not c.IsValid[rowIndex] then
Error(ER_VALUE_IS_NA, fSchema.NameAt(colIndex));
Result := c.Data[rowIndex];
end;
function DataFrame.Int(rowIndex: integer; colName: string): integer;
begin
Result := Int(rowIndex, ColumnIndex(colName));
end;
function DataFrame.Float(name: string): array of real;
begin
Result := GetFloatColumn(name);
end;
function DataFrame.Float(rowIndex, colIndex: integer): real;
begin
CheckRowIndex(rowIndex);
CheckColumnIndex(colIndex);
var c := FloatColumn(columns[colIndex]);
if not c.IsValid[rowIndex] then
Error(ER_VALUE_IS_NA, fSchema.NameAt(colIndex));
Result := c.Data[rowIndex];
end;
function DataFrame.Float(rowIndex: integer; colName: string): real;
begin
Result := Float(rowIndex, ColumnIndex(colName));
end;
function DataFrame.Str(name: string): array of string;
begin
Result := GetStrColumn(name);
end;
function DataFrame.Str(rowIndex, colIndex: integer): string;
begin
CheckRowIndex(rowIndex);
CheckColumnIndex(colIndex);
var c := StrColumn(columns[colIndex]);
if not c.IsValid[rowIndex] then
Error(ER_VALUE_IS_NA, fSchema.NameAt(colIndex));
Result := c.Data[rowIndex];
end;
function DataFrame.Str(rowIndex: integer; colName: string): string;
begin
Result := Str(rowIndex, ColumnIndex(colName));
end;
function DataFrame.Bool(name: string): array of boolean;
begin
Result := GetBoolColumn(name);
end;
function DataFrame.Bool(rowIndex, colIndex: integer): boolean;
begin
CheckRowIndex(rowIndex);
CheckColumnIndex(colIndex);
var c := BoolColumn(columns[colIndex]);
if not c.IsValid[rowIndex] then
Error(ER_VALUE_IS_NA, fSchema.NameAt(colIndex));
Result := c.Data[rowIndex];
end;
function DataFrame.Bool(rowIndex: integer; colName: string): boolean;
begin
Result := Bool(rowIndex, ColumnIndex(colName));
end;
function DataFrame.DateTime(name: string): array of System.DateTime;
begin
Result := GetDateTimeColumn(name);
end;
function DataFrame.DateTime(rowIndex, colIndex: integer): System.DateTime;
begin
CheckRowIndex(rowIndex);
CheckColumnIndex(colIndex);
var c := DateTimeColumn(columns[colIndex]);
if not c.IsValid[rowIndex] then
Error(ER_VALUE_IS_NA, fSchema.NameAt(colIndex));
Result := c.Data[rowIndex];
end;
function DataFrame.DateTime(rowIndex: integer; colName: string): System.DateTime;
begin
Result := DateTime(rowIndex, ColumnIndex(colName));
end;
function DataFrame.TrainTestSplit(testRatio: real; shuffle: boolean; seed: integer): (DataFrame, DataFrame);
begin
if Self = nil then

View file

@ -42,7 +42,7 @@ type
public
/// Возвращает количество столбцов в схеме
property ColumnCount: integer read fNames.Length;
/// Возвращает копию массива имён столбцов
/// Возвращает имена столбцов
property ColumnNames: array of string read GetColumnNames;
/// Возвращает копию массива типов столбцов
property Types: array of ColumnType read GetTypes;

View file

@ -146,6 +146,8 @@ type
/// Создаёт матрицу из двумерного массива вещественных значений.
constructor Create(values: array[,] of real);
constructor Create(values: array of Vector);
/// Возвращает копию данных в виде двумерного массива.
function ToArray2D: array[,] of real;
/// Возвращает строку матрицы в виде массива.
@ -225,6 +227,7 @@ type
static function operator implicit(a: array [,] of real): Matrix := new Matrix(a);
static function operator implicit(a: array of Vector): Matrix := new Matrix(a);
static function operator implicit(a: array of array of real): Matrix := new Matrix(Matr(a));
static function operator implicit(a: array of array of integer): Matrix := new Matrix(Matr(a.ConvertAll(x -> x.ConvertAll(y -> real(y)))));
@ -369,8 +372,14 @@ const
'Длина вектора должна быть неотрицательной!!Vector length must be non-negative';
ER_VALUES_NULL =
'values не может быть nil!!values cannot be nil';
ER_VALUES_EMPTY =
'values не может быть пустым!!values cannot be empty';
ER_VECTOR_LENGTH_MISMATCH =
'Несоответствие длины векторов: {0} и {1}!!Vector length mismatch: {0} vs {1}';
ER_VECTOR_NULL =
'Вектор не может быть nil!!vector cannot be nil';
ER_MATRIX_ROWS_HAVE_DIFFERENT_LENGTHS =
'Строки матрицы имеют разную длину!!matrix rows have different lengths';
ER_VECTOR_EMPTY =
'Вектор пуст!!Vector is empty';
ER_VECTOR_DIVIDE_BY_ZERO =
@ -659,6 +668,38 @@ begin
fdata := Copy(values);
end;
constructor Matrix.Create(values: array of Vector);
begin
if values = nil then
ArgumentNullError(ER_VALUES_NULL);
var r := values.Length;
if r = 0 then
ArgumentError(ER_VALUES_EMPTY);
if values[0] = nil then
ArgumentNullError(ER_VECTOR_NULL);
var c := values[0].Length;
if c = 0 then
ArgumentError(ER_VECTOR_EMPTY);
for var i := 1 to r - 1 do
begin
if values[i] = nil then
ArgumentNullError(ER_VECTOR_NULL);
if values[i].Length <> c then
ArgumentError(ER_MATRIX_ROWS_HAVE_DIFFERENT_LENGTHS);
end;
fdata := new real[r, c];
for var i := 0 to r - 1 do
for var j := 0 to c - 1 do
fdata[i, j] := values[i][j];
end;
function Matrix.ToArray2D: array[,] of real;
begin
Result := Copy(fdata);
@ -1094,7 +1135,6 @@ begin
end;
static function Matrix.operator +(A, B: Matrix): Matrix;
begin
CheckSameSize(A, B);

View file

@ -115,6 +115,7 @@ type
CsvLoader = DataFrameABC.CsvLoader;
JoinKind = DataFrameABC.JoinKind;
GroupView = DataFrameABC.GroupView;
EncodedTarget = DataAdapters.EncodedTarget;
IProbabilisticClassifier = MLCoreABC.IProbabilisticClassifier;
IClassifier = MLCoreABC.IClassifier;
@ -135,6 +136,8 @@ type
ClassificationMatrixPipeline = MLModelsABC.ClassificationMatrixPipeline;
RegressionMatrixPipeline = MLModelsABC.RegressionMatrixPipeline;
ClusteringMatrixPipeline = MLModelsABC.ClusteringMatrixPipeline;
LinearDecisionBoundary = MLModelsABC.LinearDecisionBoundary;
LinearRegressionLine = MLModelsABC.LinearRegressionLine;
LinearRegression = MLModelsABC.LinearRegression;
LogisticRegression = MLModelsABC.LogisticRegression;
@ -143,12 +146,14 @@ type
ElasticNet = MLModelsABC.ElasticNet;
DecisionTreeClassifier = MLModelsABC.DecisionTreeClassifier;
DecisionTreeRegressor = MLModelsABC.DecisionTreeRegressor;
DecisionTreeView = MLModelsABC.DecisionTreeView;
RandomForestRegressor = MLModelsABC.RandomForestRegressor;
RandomForestClassifier = MLModelsABC.RandomForestClassifier;
GradientBoostingRegressor = MLModelsABC.GradientBoostingRegressor;
GradientBoostingClassifier = MLModelsABC.GradientBoostingClassifier;
KNNClassifier = MLModelsABC.KNNClassifier;
KNNRegressor = MLModelsABC.KNNRegressor;
NeighborInfo = MLModelsABC.NeighborInfo;
KMeans = MLModelsABC.KMeans;
DBSCAN = MLModelsABC.DBSCAN;

View file

@ -110,6 +110,10 @@ type
/// и целевых метках классов y.
function Fit(X: Matrix; y: array of integer): IClassifier;
/// Возвращает метку класса для одного объекта X
/// в том же виде, в каком метки были поданы модели при обучении.
function PredictOne(X: Vector): integer;
/// Возвращает метки классов в том же виде, в каком они были поданы модели при обучении.
function Predict(X: Matrix): array of integer;
@ -131,6 +135,10 @@ type
/// Позволяет получать значения в диапазоне (0, 1)
/// вместо только итогового решения.
IProbabilisticClassifier = interface(IClassifier)
/// Возвращает вектор вероятностей классов для одного объекта X.
/// Порядок элементов соответствует GetClassLabels.
function PredictProbaOne(X: Vector): Vector;
/// Возвращает матрицу вероятностей классов для всех объектов из X.
/// Размер результата: nSamples × nClasses, где:
/// - nSamples число объектов в X;
@ -148,6 +156,9 @@ type
/// и векторе целевых значений y.
function Fit(X: Matrix; y: Vector): IRegressor;
/// Возвращает числовое предсказание для одного объекта X.
function PredictOne(X: Vector): real;
function Predict(X: Matrix): Vector;
end;

View file

@ -105,6 +105,24 @@ type
function GetClassLabels: array of string;
end;
LinearDecisionBoundary = record
A, B, C: real;
function Evaluate(x, y: real): real;
function Y(x: real): real;
function Side(x, y: real): integer;
function Distance(x, y: real): real;
function ToString: string; override;
function Coefficients: (real,real,real) := (A,B,C);
end;
LinearRegressionLine = record
K, B: real;
function Evaluate(x: real): real;
function Y(x: real): real;
function ToString: string; override;
function Coefficients: (real,real) := (K,B);
end;
/// Линейная регрессионная модель (метод наименьших квадратов).
/// Предсказывает числовое значение по линейной комбинации признаков
/// Используется в задачах регрессии при отсутствии выраженной
@ -125,6 +143,9 @@ type
/// обученное состояние модели НЕ копируется методом Clone
function Fit(X: Matrix; y: Vector): IRegressor;
/// Возвращает числовое предсказание для одного объекта X.
function PredictOne(X: Vector): real;
/// Предсказывает значения для матрицы признаков
/// Возвращает вектор длины m
function Predict(X: Matrix): Vector;
@ -144,6 +165,10 @@ type
/// Используется для проверки корректности вызова Predict.
property IsFitted: boolean read fFitted;
/// Возвращает уравнение регрессионной прямой y = k*x + b
/// для модели с одним признаком.
function RegressionLine: LinearRegressionLine;
function ToString: string; override;
/// Копирует только конфигурацию модели (без обученного состояния).
@ -179,6 +204,9 @@ type
/// обученное состояние модели НЕ копируется методом Clone
function Fit(X: Matrix; y: Vector): IRegressor;
/// Возвращает числовое предсказание для одного объекта X.
function PredictOne(X: Vector): real;
/// Предсказывает непрерывные значения для объектов X.
/// Результат вектор длины m.
function Predict(X: Matrix): Vector;
@ -248,6 +276,9 @@ type
/// обученное состояние модели НЕ копируется методом Clone
function Fit(X: Matrix; y: Vector): IRegressor;
/// Возвращает числовое предсказание для одного объекта X.
function PredictOne(X: Vector): real;
/// Предсказывает непрерывные значения для объектов X.
/// Результат вектор длины m.
function Predict(X: Matrix): Vector;
@ -307,6 +338,9 @@ type
/// обученное состояние модели НЕ копируется методом Clone
function Fit(X: Matrix; y: Vector): IRegressor;
/// Возвращает числовое предсказание для одного объекта X.
function PredictOne(X: Vector): real;
/// Предсказывает значения целевой переменной для входных данных.
/// X матрица признаков размера [nSamples x nFeatures].
/// Возвращает вектор предсказаний длины nSamples.
@ -384,6 +418,18 @@ type
/// обученное состояние модели НЕ копируется методом Clone
function Fit(X: Matrix; y: array of integer): IClassifier;
/// Возвращает метку класса для одного объекта X
/// в том же виде, в каком метки были поданы модели при обучении.
function PredictOne(X: Vector): integer;
/// Возвращает матрицу вероятностей классов для всех объектов из X.
/// Размер результата: nSamples × nClasses, где:
/// - nSamples число объектов в X;
/// - nClasses число классов модели.
/// Элемент [i, k] содержит вероятность того, что объект i принадлежит классу k.
/// Сумма вероятностей в каждой строке равна 1.
function PredictProbaOne(X: Vector): Vector;
/// Возвращает матрицу вероятностей классов для всех объектов из X.
/// Размер результата: nSamples × nClasses, где:
/// - nSamples число объектов в X;
@ -416,6 +462,10 @@ type
function Name: string := Self.GetType.Name;
/// Возвращает коэффициенты разделяющей прямой A*x + B*y + C = 0
/// для бинарной задачи с двумя признаками.
function DecisionBoundary: LinearDecisionBoundary;
procedure SetClassLabels(classes: array of string);
function GetClassLabels: array of string;
@ -438,6 +488,25 @@ type
function Clone: DecisionTreeNode;
end;
/// Представление решающего дерева с именами признаков и классов.
/// Используется для печати и последующей визуализации.
DecisionTreeView = class
private
fRoot: DecisionTreeNode;
fFeatureNames: array of string;
fClassNames: array of string;
public
constructor Create(
root: DecisionTreeNode;
featureNames: array of string;
classNames: array of string
);
/// Возвращает текстовое представление дерева.
function ToString: string; override;
end;
/// Результат поиска лучшего разбиения узла дерева
SplitResult = record
/// Found = true, если допустимое разбиение найдено.
@ -584,6 +653,7 @@ type
property FeatureImportances: Vector read GetFeatureImportances;
function PredictOne(x: Vector; node: DecisionTreeNode): integer;
function RootNode: DecisionTreeNode;
function Clone: DecisionTreeCore;
end;
@ -630,6 +700,10 @@ type
/// обученное состояние дерева НЕ копируется методом Clone
function Fit(X: Matrix; y: array of integer): IClassifier;
/// Возвращает метку класса для одного объекта X
/// в том же виде, в каком метки были поданы модели при обучении.
function PredictOne(X: Vector): integer;
/// Возвращает метки классов для объектов из X
/// в том же виде, в каком они были поданы модели при обучении.
function Predict(X: Matrix): array of integer;
@ -652,6 +726,11 @@ type
function GetClassLabels: array of string;
function Tree(
featureNames: array of string;
classNames: array of string
): DecisionTreeView;
/// Возвращает true, если дерево обучено.
/// Если false Predict вызовет ошибку.
property IsFitted: boolean read fFitted;
@ -795,6 +874,9 @@ type
/// обученное состояние дерева НЕ копируется методом Clone
function Fit(X: Matrix; y: Vector): IRegressor; override;
/// Возвращает числовое предсказание для одного объекта X.
function PredictOne(X: Vector): real;
/// Выполняет предсказание для всех объектов X.
/// Возвращает вектор вещественных значений.
function Predict(X: Matrix): Vector; override;
@ -952,6 +1034,9 @@ type
/// обученное состояние ансамбля НЕ копируется методом Clone
function Fit(X: Matrix; y: Vector): IRegressor;
/// Возвращает числовое предсказание для одного объекта X.
function PredictOne(X: Vector): real;
/// Выполняет предсказание для X.
/// Итоговое значение среднее предсказаний всех деревьев ансамбля.
function Predict(X: Matrix): Vector;
@ -1005,6 +1090,10 @@ type
/// обученное состояние ансамбля НЕ копируется методом Clone
function Fit(X: Matrix; y: array of integer): IClassifier;
/// Возвращает метку класса для одного объекта X
/// в том же виде, в каком метки были поданы модели при обучении.
function PredictOne(X: Vector): integer;
/// Возвращает метки классов для объектов из X
/// в том же виде, в каком они были поданы модели при обучении.
/// Для каждого объекта агрегируются предсказания всех деревьев.
@ -1016,6 +1105,14 @@ type
/// Требует предварительного вызова Fit.
function PredictLabels(X: Matrix): array of string;
/// Возвращает матрицу вероятностей классов для всех объектов из X.
/// Размер результата: nSamples × nClasses, где:
/// - nSamples число объектов в X;
/// - nClasses число классов модели.
/// Элемент [i, k] содержит вероятность того, что объект i принадлежит классу k.
/// Сумма вероятностей в каждой строке равна 1.
function PredictProbaOne(X: Vector): Vector;
/// Возвращает матрицу вероятностей классов для всех объектов из X.
/// Размер результата: nSamples × nClasses, где:
/// - nSamples число объектов в X;
@ -1176,6 +1273,9 @@ type
/// обученное состояние модели НЕ копируется методом Clone
function Fit(X: Matrix; y: Vector): IRegressor;
/// Возвращает числовое предсказание для одного объекта X.
function PredictOne(X: Vector): real;
/// Предсказывает значения целевой переменной.
/// Используются все обученные деревья.
function Predict(X: Matrix): Vector;
@ -1326,6 +1426,10 @@ type
/// обученное состояние модели НЕ копируется методом Clone
function Fit(X: Matrix; y: array of integer): IClassifier;
/// Возвращает метку класса для одного объекта X
/// в том же виде, в каком метки были поданы модели при обучении.
function PredictOne(X: Vector): integer;
/// Обучает модель градиентного бустинга с использованием валидационной выборки.
/// Поддерживает early stopping:
/// обучение останавливается, если метрика не улучшается в течение earlyStoppingPatience итераций.
@ -1339,6 +1443,14 @@ type
/// Возвращает исходные строковые метки классов для объектов из X.
function PredictLabels(X: Matrix): array of string;
/// Возвращает матрицу вероятностей классов для всех объектов из X.
/// Размер результата: nSamples × nClasses, где:
/// - nSamples число объектов в X;
/// - nClasses число классов модели.
/// Элемент [i, k] содержит вероятность того, что объект i принадлежит классу k.
/// Сумма вероятностей в каждой строке равна 1.
function PredictProbaOne(X: Vector): Vector;
/// Возвращает матрицу вероятностей классов для всех объектов из X.
/// Размер результата: nSamples × nClasses, где:
/// - nSamples число объектов в X;
@ -1400,6 +1512,11 @@ type
idx: integer;
end;
NeighborInfo = record
Index: integer;
Distance: real;
end;
/// Режим взвешивания в алгоритме k ближайших соседей.
/// Uniform равномерное голосование/усреднение.
/// Distance веса обратно пропорциональны расстоянию (1 / dist)
@ -1422,6 +1539,7 @@ type
// ==== common methods ====
procedure ValidatePredictInput(X: Matrix);
procedure FillNeighborDistances(x: Vector);
function SquaredL2(trainRow: integer; XTest: Matrix; testRow: integer): double;
@ -1434,6 +1552,11 @@ type
/// weighting режим взвешивания соседей
constructor Create(k: integer; weighting: KNNWeighting := KNNWeighting.Uniform);
/// Возвращает k ближайших обучающих объектов
/// для одного объекта X.
/// Индекс относится к обучающей выборке, переданной в Fit.
function GetNearestNeighbors(X: Vector): array of NeighborInfo;
/// Обучает модель k ближайших соседей.
/// X матрица m × n (m объектов, n признаков).
/// y вектор длины m с целевыми значениями.
@ -1483,6 +1606,10 @@ type
/// обученное состояние модели (обучающая выборка) НЕ копируется методом Clone
function Fit(X: Matrix; y: array of integer): IClassifier;
/// Возвращает метку класса для одного объекта X
/// в том же виде, в каком метки были поданы модели при обучении.
function PredictOne(X: Vector): integer;
/// Возвращает метки классов для объектов из X
/// в том же виде, в каком они были поданы модели при обучении.
/// Не является потокобезопасным: не вызывать одновременно из нескольких потоков
@ -1494,6 +1621,14 @@ type
/// для одного экземпляра модели.
function PredictLabels(X: Matrix): array of string;
/// Возвращает матрицу вероятностей классов для всех объектов из X.
/// Размер результата: nSamples × nClasses, где:
/// - nSamples число объектов в X;
/// - nClasses число классов модели.
/// Элемент [i, k] содержит вероятность того, что объект i принадлежит классу k.
/// Сумма вероятностей в каждой строке равна 1.
function PredictProbaOne(X: Vector): Vector;
/// Возвращает матрицу вероятностей классов для всех объектов из X.
/// Размер результата: nSamples × nClasses, где:
/// - nSamples число объектов в X;
@ -1537,6 +1672,9 @@ type
/// обученное состояние модели (обучающая выборка) НЕ копируется методом Clone
function Fit(X: Matrix; y: Vector): IRegressor;
/// Возвращает числовое предсказание для одного объекта X.
function PredictOne(X: Vector): real;
/// Выполняет предсказание числовых значений для объектов X.
/// Возвращает вектор предсказанных значений
function Predict(X: Matrix): Vector;
@ -1840,11 +1978,15 @@ type
ClassificationMatrixPipeline = class(MatrixPipeline, IProbabilisticClassifier, IClassifierInternal)
public
function Fit(X: Matrix; y: array of integer): IClassifier;
/// Возвращает метку класса для одного объекта X
/// в том же виде, в каком метки были поданы модели при обучении.
function PredictOne(X: Vector): integer;
/// Возвращает метки классов для объектов из X
/// в том же виде, в каком они были поданы модели при обучении.
function Predict(X: Matrix): array of integer; reintroduce;
/// Возвращает строковые метки классов для объектов из X.
function PredictLabels(X: Matrix): array of string;
function PredictProbaOne(X: Vector): Vector;
function PredictProba(X: Matrix): Matrix; reintroduce;
procedure SetClassLabels(classes: array of string);
function GetClassLabels: array of string;
@ -1853,6 +1995,8 @@ type
RegressionMatrixPipeline = class(MatrixPipeline, IRegressor)
public
function Fit(X: Matrix; y: Vector): IRegressor; reintroduce;
/// Возвращает числовое предсказание для одного объекта X.
function PredictOne(X: Vector): real;
end;
/// Общая база для матричных конвейеров без учителя.
@ -2413,6 +2557,18 @@ const
ER_LOGISTIC_NEED_AT_LEAST_TWO_CLASSES =
'Для LogisticRegression нужно минимум 2 класса.!!' +
'LogisticRegression requires at least 2 classes.';
ER_LOGISTIC_BOUNDARY_NEEDS_BINARY =
'DecisionBoundary доступен только для бинарной LogisticRegression.!!' +
'DecisionBoundary is available only for binary LogisticRegression.';
ER_LOGISTIC_BOUNDARY_NEEDS_2D =
'DecisionBoundary требует ровно 2 признака.!!' +
'DecisionBoundary requires exactly 2 features.';
ER_LINEAR_REGRESSION_LINE_NEEDS_1D =
'RegressionLine требует ровно 1 признак.!!' +
'RegressionLine requires exactly 1 feature.';
ER_DECISION_BOUNDARY_DEGENERATE =
'Разделяющая прямая вырождена: A и B равны нулю.!!' +
'Decision boundary is degenerate: A and B are zero.';
ER_MIN_SAMPLES_SPLIT_INVALID =
'minSamplesSplit должно быть >= 2 ({0}).!!' +
'minSamplesSplit must be >= 2 ({0}).';
@ -2481,6 +2637,17 @@ const
// Проверка на NuN/Inf
//-----------------------------
function SingleRowMatrix(X: Vector): Matrix;
begin
if X = nil then
ArgumentNullError(ER_X_NULL);
Result := new Matrix(1, X.Length);
for var j := 0 to X.Length - 1 do
Result[0, j] := X[j];
end;
procedure ClassifierLabelHelper.SetClassValues(values: array of integer);
begin
fClassValues := Copy(values);
@ -2685,6 +2852,11 @@ begin
Result := X * fcoef + fIntercept;
end;
function LinearRegression.PredictOne(X: Vector): real;
begin
Result := Predict(SingleRowMatrix(X))[0];
end;
function LinearRegression.ToString: string;
begin
Result := 'LinearRegression'
@ -2811,6 +2983,11 @@ begin
Result[i] += fIntercept;
end;
function RidgeRegression.PredictOne(X: Vector): real;
begin
Result := Predict(SingleRowMatrix(X))[0];
end;
function RidgeRegression.ToString: string;
begin
Result := 'RidgeRegression(lambda=' + fLambda + ')';
@ -2994,6 +3171,11 @@ begin
Result[i] += fIntercept;
end;
function ElasticNet.PredictOne(X: Vector): real;
begin
Result := Predict(SingleRowMatrix(X))[0];
end;
function ElasticNet.ToString: string;
begin
Result :=
@ -3033,6 +3215,11 @@ begin
Result := fModel.Predict(X);
end;
function LassoRegression.PredictOne(X: Vector): real;
begin
Result := fModel.PredictOne(X);
end;
function LassoRegression.Clone: IModel;
begin
Result := new LassoRegression(fAlpha, fMaxIter, fTol);
@ -3520,6 +3707,11 @@ begin
Result := Z;
end;
function LogisticRegression.PredictProbaOne(X: Vector): Vector;
begin
Result := PredictProba(SingleRowMatrix(X)).GetRow(0);
end;
function LogisticRegression.Predict(X: Matrix): array of integer;
begin
if not fFitted then
@ -3558,6 +3750,11 @@ begin
end;
end;
function LogisticRegression.PredictOne(X: Vector): integer;
begin
Result := Predict(SingleRowMatrix(X))[0];
end;
function LogisticRegression.PredictLabels(X: Matrix): array of string;
begin
var pred := Predict(X);
@ -3621,6 +3818,138 @@ begin
Result := fIntercept;
end;
function LinearDecisionBoundary.Evaluate(x, y: real): real;
begin
Result := A * x + B * y + C;
end;
function LinearDecisionBoundary.Y(x: real): real;
begin
if Abs(B) < 1e-12 then
Result := real.PositiveInfinity
else
Result := -(A * x + C) / B;
end;
function LinearDecisionBoundary.Side(x, y: real): integer;
begin
var v := Evaluate(x, y);
if Abs(v) < 1e-12 then
Result := 0
else if v < 0 then
Result := -1
else
Result := 1;
end;
function LinearDecisionBoundary.Distance(x, y: real): real;
begin
var denom := Sqrt(A * A + B * B);
if denom < 1e-12 then
Error(ER_DECISION_BOUNDARY_DEGENERATE);
Result := Abs(Evaluate(x, y)) / denom;
end;
function LinearDecisionBoundary.ToString: string;
function FormatCoeff(v: real): string;
begin
if Abs(v - Round(v)) < 1e-12 then
Result := Round(v).ToString
else
Result := v.ToString('0.##');
end;
function FmtSigned(v: real; suffix: string): string;
begin
if Abs(v) < 1e-12 then
exit('');
var mag := FormatCoeff(Abs(v));
if suffix <> '' then
mag += '*' + suffix;
if v < 0 then
Result := ' - ' + mag
else
Result := ' + ' + mag;
end;
begin
Result := '';
if Abs(A) >= 1e-12 then
Result := FormatCoeff(A) + '*x';
Result += FmtSigned(B, 'y');
Result += FmtSigned(C, '');
if Result = '' then
Result := '0';
if Result.StartsWith(' + ') then
Result := Result.Substring(3);
if Result.StartsWith(' - ') then
Result := '-' + Result.Substring(3);
Result += ' = 0';
end;
function LinearRegressionLine.Evaluate(x: real): real;
begin
Result := K * x + B;
end;
function LinearRegressionLine.Y(x: real): real;
begin
Result := Evaluate(x);
end;
function LinearRegressionLine.ToString: string;
function FormatCoeff(v: real): string;
begin
if Abs(v - Round(v)) < 1e-12 then
Result := Round(v).ToString
else
Result := v.ToString('0.##');
end;
begin
Result := FormatCoeff(K) + '*x';
if B > 1e-12 then
Result += ' + ' + FormatCoeff(B)
else if B < -1e-12 then
Result += ' - ' + FormatCoeff(Abs(B));
end;
function LinearRegression.RegressionLine: LinearRegressionLine;
begin
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if (fCoef = nil) or (fCoef.Length <> 1) then
ArgumentError(ER_LINEAR_REGRESSION_LINE_NEEDS_1D);
Result.K := fCoef[0];
Result.B := fIntercept;
end;
function LogisticRegression.DecisionBoundary: LinearDecisionBoundary;
begin
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if fClassCount <> 2 then
ArgumentError(ER_LOGISTIC_BOUNDARY_NEEDS_BINARY);
if fW.RowCount <> 2 then
ArgumentError(ER_LOGISTIC_BOUNDARY_NEEDS_2D);
Result.A := fW[0,0] - fW[0,1];
Result.B := fW[1,0] - fW[1,1];
Result.C := fIntercept[0] - fIntercept[1];
if (Abs(Result.A) < 1e-12) and (Abs(Result.B) < 1e-12) then
Error(ER_DECISION_BOUNDARY_DEGENERATE);
end;
function GiniCriterion.Impurity(y: Vector; indices: array of integer): real;
begin
var n := indices.Length;
@ -3756,6 +4085,116 @@ begin
Result := n;
end;
constructor DecisionTreeView.Create(
root: DecisionTreeNode;
featureNames: array of string;
classNames: array of string
);
begin
if root <> nil then
fRoot := root.Clone;
fFeatureNames := Copy(featureNames);
fClassNames := Copy(classNames);
end;
function DecisionTreeView.ToString: string;
function FormatReal(v: real): string;
begin
if Abs(v - Round(v)) < 1e-12 then
Result := Round(v).ToString
else
Result := v.ToString('0.##');
end;
function FeatureNameOf(idx: integer): string;
begin
if (idx >= 0) and (idx < fFeatureNames.Length) and (fFeatureNames[idx] <> nil) and (fFeatureNames[idx] <> '') then
Result := fFeatureNames[idx]
else
Result := 'x' + idx;
end;
function ClassNameOf(idx: integer): string;
begin
if (idx >= 0) and (idx < fClassNames.Length) then
Result := fClassNames[idx]
else
Result := idx.ToString;
end;
function LeafText(node: DecisionTreeNode): string;
begin
if node = nil then
exit('nil');
if fClassNames <> nil then
Result := ClassNameOf(Round(node.LeafValue))
else
Result := FormatReal(node.LeafValue);
end;
procedure AppendBranch(sb: System.Text.StringBuilder; node: DecisionTreeNode;
prefix: string; isLast: boolean; branchMark: string);
begin
sb.Append(prefix);
if isLast then
sb.Append('└── ')
else
sb.Append('├── ');
if node = nil then
begin
sb.Append(branchMark + ' → nil');
sb.AppendLine;
exit;
end;
if node.IsLeaf then
begin
sb.Append(branchMark + ' → ' + LeafText(node));
sb.AppendLine;
exit;
end;
sb.Append(branchMark + ' ');
sb.AppendLine;
var childPrefix := prefix;
if isLast then
childPrefix += ' '
else
childPrefix += '│ ';
sb.Append(childPrefix);
sb.Append(FeatureNameOf(node.FeatureIndex));
sb.Append(' ≤ ');
sb.Append(FormatReal(node.Threshold));
sb.Append('?');
sb.AppendLine;
AppendBranch(sb, node.Left, childPrefix, false, '✓');
AppendBranch(sb, node.Right, childPrefix, true, '✗');
end;
begin
if fRoot = nil then
exit('DecisionTreeView(empty)');
if fRoot.IsLeaf then
exit('→ ' + LeafText(fRoot));
var sb := new System.Text.StringBuilder;
sb.Append(FeatureNameOf(fRoot.FeatureIndex));
sb.Append(' ≤ ');
sb.Append(FormatReal(fRoot.Threshold));
sb.Append('?');
sb.AppendLine;
AppendBranch(sb, fRoot.Left, '', false, '✓');
AppendBranch(sb, fRoot.Right, '', true, '✗');
Result := sb.ToString.TrimEnd;
end;
const MAX_ALLOWED_TREE_DEPTH = 1000;
// DecisionTreeCore
@ -3852,6 +4291,13 @@ begin
Result := PredictOne(x, node.Right);
end;
function DecisionTreeCore.RootNode: DecisionTreeNode;
begin
if fRoot = nil then
NotFittedError(ER_FIT_NOT_CALLED);
Result := fRoot.Clone;
end;
function DecisionTreeCore.MajorityClass(y: Vector; indices: array of integer): integer;
begin
var counts := new integer[fClassCount];
@ -4816,6 +5262,11 @@ begin
Result := fLabels.DecodePredictions(fCore.Predict(X));
end;
function DecisionTreeClassifier.PredictOne(X: Vector): integer;
begin
Result := Predict(SingleRowMatrix(X))[0];
end;
function DecisionTreeClassifier.PredictLabels(X: Matrix): array of string;
begin
if fLabels = nil then
@ -4841,6 +5292,21 @@ begin
Result := fLabels.GetClassLabels;
end;
function DecisionTreeClassifier.Tree(
featureNames: array of string;
classNames: array of string
): DecisionTreeView;
begin
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
Result := new DecisionTreeView(
fCore.RootNode,
featureNames,
classNames
);
end;
function DecisionTreeClassifier.Clone: IModel;
begin
Result := new DecisionTreeClassifier(
@ -5017,6 +5483,11 @@ begin
Result[i] := PredictOne(X, i);
end;
function DecisionTreeRegressor.PredictOne(X: Vector): real;
begin
Result := Predict(SingleRowMatrix(X))[0];
end;
function DecisionTreeRegressor.Clone: IModel;
begin
Result := new DecisionTreeRegressor(
@ -5278,6 +5749,11 @@ begin
Result := resultVec;
end;
function RandomForestRegressor.PredictOne(X: Vector): real;
begin
Result := Predict(SingleRowMatrix(X))[0];
end;
function RandomForestRegressor.Clone: IModel;
begin
Result := new RandomForestRegressor(
@ -5593,6 +6069,11 @@ begin
end;
end;
function RandomForestClassifier.PredictProbaOne(X: Vector): Vector;
begin
Result := PredictProba(SingleRowMatrix(X)).GetRow(0);
end;
function RandomForestClassifier.Clone: IModel;
begin
Result := new RandomForestClassifier(
@ -6239,6 +6720,11 @@ begin
Result := yPred;
end;
function GradientBoostingRegressor.PredictOne(X: Vector): real;
begin
Result := Predict(SingleRowMatrix(X))[0];
end;
function GradientBoostingRegressor.PredictStage(X: Matrix; m: integer): Vector;
begin
if not fFitted then
@ -7062,6 +7548,11 @@ begin
Result := probs;
end;
function GradientBoostingClassifier.PredictProbaOne(X: Vector): Vector;
begin
Result := PredictProba(SingleRowMatrix(X)).GetRow(0);
end;
function GradientBoostingClassifier.PredictStageProba(
X: Matrix; m: integer): Matrix;
begin
@ -7231,6 +7722,11 @@ begin
Result[i] := fLabels.ClassValueAt(probs.RowArgMax(i));
end;
function GradientBoostingClassifier.PredictOne(X: Vector): integer;
begin
Result := Predict(SingleRowMatrix(X))[0];
end;
function GradientBoostingClassifier.PredictLabels(X: Matrix): array of string;
begin
var pred := Predict(X);
@ -7271,6 +7767,25 @@ begin
fFitted := False;
end;
function KNNBase.GetNearestNeighbors(X: Vector): array of NeighborInfo;
begin
FillNeighborDistances(X);
QuickSelect(fK - 1);
var resList := new List<NeighborInfo>;
for var i := 0 to fK - 1 do
begin
var item: NeighborInfo;
item.Index := fNeighbors[i].idx;
item.Distance := Sqrt(fNeighbors[i].dist);
resList.Add(item);
end;
resList.Sort((a, b) -> a.Distance.CompareTo(b.Distance));
Result := resList.ToArray;
end;
procedure KNNBase.ValidatePredictInput(X: Matrix);
begin
if X = nil then
@ -7283,6 +7798,37 @@ begin
DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fXTrain.ColCount);
end;
procedure KNNBase.FillNeighborDistances(x: Vector);
begin
if x = nil then
ArgumentNullError(ER_X_NULL);
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if x.Length <> fXTrain.ColCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH, x.Length, fXTrain.ColCount);
var n := fXTrain.RowCount;
var p := fXTrain.ColCount;
var trainRows := fXTrain.Data.Rows;
for var t := 0 to n - 1 do
begin
var rowTrain := trainRows[t];
var sum := 0.0;
for var j := 0 to p - 1 do
begin
var diff := rowTrain[j] - x[j];
sum += diff * diff;
end;
fNeighbors[t].dist := sum;
fNeighbors[t].idx := t;
end;
end;
function KNNBase.SquaredL2(trainRow: integer; XTest: Matrix; testRow: integer): double;
begin
var sum := 0.0;
@ -7307,16 +7853,14 @@ begin
var left := 0;
var right := fNeighbors.Length - 1;
while true do
while left < right do
begin
var pivotIndex := Partition(left, right);
var p := Partition(left, right);
if pivotIndex = k then
exit
else if pivotIndex > k then
right := pivotIndex - 1
if k <= p then
right := p
else
left := pivotIndex + 1;
left := p + 1;
end;
end;
@ -7656,6 +8200,16 @@ begin
end;
end;
function KNNClassifier.PredictOne(X: Vector): integer;
begin
Result := Predict(SingleRowMatrix(X))[0];
end;
function RandomForestClassifier.PredictOne(X: Vector): integer;
begin
Result := Predict(SingleRowMatrix(X))[0];
end;
function KNNClassifier.PredictLabels(X: Matrix): array of string;
begin
var pred := Predict(X);
@ -7808,6 +8362,11 @@ begin
end;
end;
function KNNClassifier.PredictProbaOne(X: Vector): Vector;
begin
Result := PredictProba(SingleRowMatrix(X)).GetRow(0);
end;
procedure KNNClassifier.SetClassLabels(classes: array of string);
begin
if fLabels = nil then
@ -7959,6 +8518,11 @@ begin
end;
end;
function KNNRegressor.PredictOne(X: Vector): real;
begin
Result := Predict(SingleRowMatrix(X))[0];
end;
function KNNRegressor.Clone: IModel;
begin
Result := new KNNRegressor(fK, fWeighting);
@ -8722,6 +9286,11 @@ begin
Result := Self;
end;
function RegressionMatrixPipeline.PredictOne(X: Vector): real;
begin
Result := Predict(SingleRowMatrix(X))[0];
end;
function ClassificationMatrixPipeline.Predict(X: Matrix): array of integer;
begin
if not fFitted then
@ -8737,6 +9306,11 @@ begin
Result := (fModel as IClassifier).Predict(Xt);
end;
function ClassificationMatrixPipeline.PredictOne(X: Vector): integer;
begin
Result := Predict(SingleRowMatrix(X))[0];
end;
function ClassificationMatrixPipeline.PredictLabels(X: Matrix): array of string;
begin
if not fFitted then
@ -8767,6 +9341,11 @@ begin
Result := (fModel as IProbabilisticClassifier).PredictProba(Xt);
end;
function ClassificationMatrixPipeline.PredictProbaOne(X: Vector): Vector;
begin
Result := PredictProba(SingleRowMatrix(X)).GetRow(0);
end;
procedure ClassificationMatrixPipeline.SetClassLabels(classes: array of string);
begin
if (fModel = nil) or not (fModel is IClassifierInternal) then

View file

@ -84,7 +84,7 @@ type
/// Доля правильных предсказаний.
/// Показывает, какая часть объектов классифицирована верно.
/// Удобна, когда классы примерно сбалансированы.
/// Пример: Accuracy = 0.9 означает 90% правильных ответов.
/// Пример: Accuracy = 0.9 означает 90% правильных ответов
static function Accuracy(yTrue, yPred: array of integer): real;
/// Точность (Precision) для положительного класса (1).
@ -228,6 +228,7 @@ type
fClassCount: integer;
fLabels: array of integer;
fClassNames: array of string;
fLabelToIndex: Dictionary<integer, integer>;
procedure EnsureBinary;
@ -255,6 +256,10 @@ type
/// Для бинарной классификации дополнительно доступны значения TP, TN, FP и FN
constructor Create(yTrue, yPred: array of integer);
/// Создаёт матрицу ошибок и сохраняет отображаемые имена классов.
/// classNames[i] соответствует классу fLabels[i].
constructor Create(yTrue, yPred: array of integer; classNames: array of string);
/// Возвращает исходную метку класса по его внутреннему номеру.
///
/// Внутренние номера классов используются внутри матрицы ошибок
@ -264,6 +269,10 @@ type
/// данный внутренний номер.
function GetLabel(c: integer): integer;
/// Возвращает отображаемое имя класса по его внутреннему номеру.
/// Если строковые имена не заданы, возвращает исходную числовую метку как строку.
function GetClassDisplayName(i: integer): string;
/// Возвращает внутренний номер класса по его исходной метке.
///
/// Результат находится в диапазоне от 0 до ClassCount-1
@ -400,6 +409,12 @@ type
/// Применима для многоклассовой классификации.
function F1ForClass(c: integer): real;
/// Возвращает текстовое представление матрицы ошибок.
function ToString: string; override;
/// Печатает матрицу ошибок.
procedure Println;
end;
/// Метрики для оценки качества классификационных моделей.
@ -617,6 +632,8 @@ const
'Вероятность вне диапазона [0,1]: {0}!!Probability out of range [0,1]: {0}';
ER_ARG_OUT_OF_RANGE =
'Аргумент {0} имеет недопустимое значение: {1}!!Argument {0} is out of range: {1}';
ER_CLASS_NAMES_LENGTH_MISMATCH =
'Число имён классов ({0}) не совпадает с числом классов ({1})!!Class names count ({0}) does not match class count ({1})';
//-----------------------------
// Metrics
@ -1718,7 +1735,7 @@ begin
// --- собираем строки классов
for var c := 0 to cm.ClassCount - 1 do
begin
var cls := cm.GetLabel(c).ToString;
var cls := cm.GetClassDisplayName(c);
var p := cm.PrecisionForClass(c);
var r := cm.RecallForClass(c);
@ -1875,6 +1892,19 @@ begin
end;
end;
constructor ConfusionMatrix.Create(yTrue, yPred: array of integer; classNames: array of string);
begin
Create(yTrue, yPred);
if classNames = nil then
exit;
if classNames.Length <> fClassCount then
ArgumentError(ER_CLASS_NAMES_LENGTH_MISMATCH, classNames.Length, fClassCount);
fClassNames := Copy(classNames);
end;
function ConfusionMatrix.GetMatrix: array[,] of integer;
begin
Result := new integer[fClassCount, fClassCount];
@ -1892,6 +1922,16 @@ begin
Result := fLabels[c];
end;
function ConfusionMatrix.GetClassDisplayName(i: integer): string;
begin
EnsureClassIndex(i);
if (fClassNames <> nil) and (i < fClassNames.Length) then
Result := fClassNames[i]
else
Result := fLabels[i].ToString;
end;
function ConfusionMatrix.GetIndex(labl: integer): integer;
begin
if labl not in fLabelToIndex then
@ -2074,4 +2114,78 @@ begin
Result := 2 * p * r / (p + r);
end;
function ConfusionMatrix.ToString: string;
function Center(s: string; w: integer): string;
begin
if s.Length >= w then
exit(s);
var left := (w - s.Length) div 2;
var right := w - s.Length - left;
Result := new string(' ', left) + s + new string(' ', right);
end;
begin
var names := new string[fClassCount];
var rowHeader := 'Реально ↓';
var topHeader := 'Предсказано →';
var nameWidth := rowHeader.Length;
for var i := 0 to fClassCount - 1 do
begin
names[i] := GetClassDisplayName(i);
if names[i].Length > nameWidth then
nameWidth := names[i].Length;
end;
var cellWidth := 1;
for var i := 0 to fClassCount - 1 do
for var j := 0 to fClassCount - 1 do
if fMatrix[i, j].ToString.Length > cellWidth then
cellWidth := fMatrix[i, j].ToString.Length;
for var i := 0 to fClassCount - 1 do
if names[i].Length > cellWidth then
cellWidth := names[i].Length;
var leftPad := nameWidth + 3;
var firstColStart := leftPad;
var sb := new System.Text.StringBuilder;
sb.Append(new string(' ', firstColStart));
sb.AppendLine(topHeader);
sb.Append(rowHeader.PadRight(nameWidth));
sb.Append(' ');
for var j := 0 to fClassCount - 1 do
begin
if j > 0 then
sb.Append(' ');
sb.Append(Center(names[j], cellWidth));
end;
sb.AppendLine;
for var i := 0 to fClassCount - 1 do
begin
sb.Append(names[i].PadRight(nameWidth));
sb.Append(' ');
for var j := 0 to fClassCount - 1 do
begin
if j > 0 then
sb.Append(' ');
sb.Append(Center(fMatrix[i, j].ToString, cellWidth));
end;
sb.AppendLine;
end;
Result := sb.ToString.TrimEnd;
end;
procedure ConfusionMatrix.Println;
begin
System.Console.WriteLine(ToString);
end;
end.

View file

@ -94,8 +94,16 @@ type
public
constructor Create(g: GridWPF; r,c: integer);
function PaletteColor(i: integer): ColorWPF;
procedure LineGraph(x, y: array of real; color: ColorWPF := DefaultColor;
thickness: real := 2; legend: string := nil);
procedure Line(k, b: real; color: ColorWPF := DefaultColor;
thickness: real := 2; legend: string := nil);
procedure Line(A, B, C: real; color: ColorWPF := DefaultColor;
thickness: real := 2; legend: string := nil);
procedure Point(x, y: real; color: ColorWPF := DefaultColor;
size: real := 8; marker: MarkerType := MarkerType.Circle; legend: string := nil);
procedure Points(x, y: array of real; color: ColorWPF := DefaultColor;
size: real := 6; marker: MarkerType := MarkerType.Circle; legend: string := nil);
procedure Points(x, y: array of real; labels: array of integer;
@ -173,6 +181,8 @@ type
static procedure DrawLine(chart: ChartWPF; x, y: array of real;
color: ColorWPF; thickness: real; legend: string);
static procedure DrawImplicitLine(chart: ChartWPF; A, B, C: real;
color: ColorWPF; thickness: real; legend: string);
static procedure DrawText(chart: ChartWPF; s: string; x, y: real);
@ -207,8 +217,16 @@ type
public
static procedure AddSeries(chart: ChartWPF; series: UIElement);
static function PaletteColor(i: integer): ColorWPF;
static procedure LineGraph(x, y: array of real;
color: ColorWPF := DefaultColor; thickness: real := 2; legend: string := nil);
static procedure Line(k, b: real;
color: ColorWPF := DefaultColor; thickness: real := 2; legend: string := nil);
static procedure Line(A, B, C: real;
color: ColorWPF := DefaultColor; thickness: real := 2; legend: string := nil);
static procedure Point(x, y: real;
color: ColorWPF := DefaultColor; size: real := 8; marker: MarkerType := MarkerType.Circle; legend: string := nil);
static procedure Points(x, y: array of real;
color: ColorWPF := DefaultColor; size: real := 6; marker: MarkerType := MarkerType.Circle; legend: string := nil);
@ -335,6 +353,24 @@ type
property MaxCount: integer read fMaxCount;
end;
ImplicitLinePlot = class(PlotWPF)
private
fA, fB, fC: real;
fStroke: BrushWPF;
fStrokeThickness: real;
fDescription: string;
function IsFinite(x: real): boolean;
procedure OnPlotTransformChanged(sender: object; e: EventArgs);
protected
function ComputeBounds: InteractiveDataDisplay.WPF.DataRect; override;
procedure OnRender(dc: DrawingContext); override;
public
constructor Create(A, B, C: real; color: ColorWPF; thickness: real);
property Description: string read fDescription write fDescription;
end;
HorizontalBarPlot = class(PlotWPF)
private
fElements: List<UIElement> := new List<UIElement>;
@ -493,6 +529,19 @@ begin
Result := c;
end;
function ColorFromPalette(p: PaletteWPF; i: integer): ColorWPF;
begin
if (p = nil) or (p.Colors = nil) or (p.Colors.Length = 0) then
exit(Colors.Black);
var n := p.Colors.Length;
var k := i mod n;
if k < 0 then
k += n;
Result := p.Colors[k];
end;
function Clamp01(x: real): real;
begin
if x < 0 then
@ -950,6 +999,11 @@ begin
Result := c;
end;
function Cell.PaletteColor(i: integer): ColorWPF;
begin
Result := ColorFromPalette(palette, i);
end;
procedure Cell.SetPalette(p: PaletteWPF);
begin
if p = nil then exit;
@ -1108,6 +1162,29 @@ begin
end);
end;
procedure Cell.Line(k, b: real; color: ColorWPF; thickness: real; legend: string);
begin
Line(k, -1, b, color, thickness, legend);
end;
procedure Cell.Line(A, B, C: real; color: ColorWPF; thickness: real; legend: string);
begin
Plot.RunUI(() ->
begin
EnsureChart;
var clr := if color<>DefaultColor then color else Colors.Red;
Plot.DrawImplicitLine(chart, A, B, C, clr, thickness, legend);
end);
end;
procedure Cell.Point(x, y: real; color: ColorWPF; size: real; marker: MarkerType; legend: string);
begin
var clr := if color<>DefaultColor then color else Colors.Black;
Points(Arr(x), Arr(y), clr, size, marker, legend);
end;
procedure Cell.Points(x, y: array of real; color: ColorWPF;
size: real; marker: MarkerType; legend: string);
begin
@ -1631,6 +1708,20 @@ begin
AddSeries(chart, g);
end;
static procedure Plot.DrawImplicitLine(chart: ChartWPF; A, B, C: real;
color: ColorWPF; thickness: real; legend: string);
begin
var g := new ImplicitLinePlot(A, B, C, color, thickness);
if legend <> nil then
begin
g.Description := legend;
chart.LegendVisibility := Visibility.Visible;
end;
AddSeries(chart, g);
end;
static procedure Plot.DrawText(chart: ChartWPF; s: string; x, y: real);
begin
var tb := new System.Windows.Controls.TextBlock;
@ -1987,6 +2078,11 @@ begin
container.Children.Add(series);
end;
class function Plot.PaletteColor(i: integer): ColorWPF;
begin
Result := ColorFromPalette(currentPalette, i);
end;
class procedure Plot.LineGraph(x, y: array of real;
color: ColorWPF; thickness: real; legend: string);
begin
@ -1998,6 +2094,27 @@ begin
end);
end;
class procedure Plot.Line(k, b: real; color: ColorWPF; thickness: real; legend: string);
begin
Line(k, -1, b, color, thickness, legend);
end;
class procedure Plot.Line(A, B, C: real; color: ColorWPF; thickness: real; legend: string);
begin
RunUI(() ->
begin
var clr := if color<>DefaultColor then color else Colors.Red;
DrawImplicitLine(rootChart, A, B, C, clr, thickness, legend);
end);
end;
class procedure Plot.Point(x, y: real; color: ColorWPF; size: real; marker: MarkerType; legend: string);
begin
var clr := if color<>DefaultColor then color else Colors.Black;
Points(Arr(x), Arr(y), clr, size, marker, legend);
end;
static procedure Plot.Points(x, y: array of real;
color: ColorWPF; size: real; marker: MarkerType; legend: string);
begin
@ -2722,6 +2839,94 @@ begin
fMaxCount := counts.Max;
end;
constructor ImplicitLinePlot.Create(A, B, C: real; color: ColorWPF; thickness: real);
begin
fA := A;
fB := B;
fC := C;
fStroke := new SolidColorBrush(color);
fStrokeThickness := thickness;
IsAutoFitEnabled := false;
PlotTransformChanged += OnPlotTransformChanged;
end;
function ImplicitLinePlot.IsFinite(x: real): boolean;
begin
Result := not real.IsNaN(x) and not real.IsInfinity(x);
end;
procedure ImplicitLinePlot.OnPlotTransformChanged(sender: object; e: EventArgs);
begin
InvalidateVisual;
end;
function ImplicitLinePlot.ComputeBounds: InteractiveDataDisplay.WPF.DataRect;
begin
Result := InteractiveDataDisplay.WPF.DataRect.Empty;
end;
procedure ImplicitLinePlot.OnRender(dc: DrawingContext);
begin
inherited OnRender(dc);
if (Abs(fA) < 1e-12) and (Abs(fB) < 1e-12) then
exit;
var r := ActualPlotRect;
if r.IsEmpty then
exit;
var xmin := r.XMin;
var xmax := r.XMax;
var ymin := r.YMin;
var ymax := r.YMax;
var pts := new List<Point>;
if Abs(fB) > 1e-12 then
begin
var y := (-fA * xmin - fC) / fB;
if IsFinite(y) and (y >= ymin - 1e-9) and (y <= ymax + 1e-9) then
pts.Add(new Point(xmin, y));
y := (-fA * xmax - fC) / fB;
if IsFinite(y) and (y >= ymin - 1e-9) and (y <= ymax + 1e-9) then
pts.Add(new Point(xmax, y));
end;
if Abs(fA) > 1e-12 then
begin
var x := (-fB * ymin - fC) / fA;
if IsFinite(x) and (x >= xmin - 1e-9) and (x <= xmax + 1e-9) then
pts.Add(new Point(x, ymin));
x := (-fB * ymax - fC) / fA;
if IsFinite(x) and (x >= xmin - 1e-9) and (x <= xmax + 1e-9) then
pts.Add(new Point(x, ymax));
end;
if pts.Count < 2 then
exit;
var p1 := pts[0];
var p2 := pts[1];
for var i := 1 to pts.Count - 1 do
if Abs(pts[i].X - p1.X) + Abs(pts[i].Y - p1.Y) > 1e-9 then
begin
p2 := pts[i];
break;
end;
if Abs(p2.X - p1.X) + Abs(p2.Y - p1.Y) <= 1e-9 then
exit;
var s1 := new Point(LeftFromX(p1.X), TopFromY(p1.Y));
var s2 := new Point(LeftFromX(p2.X), TopFromY(p2.Y));
dc.DrawLine(new Pen(fStroke, fStrokeThickness), s1, s2);
end;
constructor HorizontalBarPlot.Create;
begin
IsAutoFitEnabled := false;