From 463d7c30ef0f2879de6baa1f910bca83486cf13e Mon Sep 17 00:00:00 2001 From: Mikhalkovich Stanislav Date: Wed, 11 Mar 2026 10:50:15 +0300 Subject: [PATCH] DataAdapters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dataset iris.csv PrintPreview поправил Класс Dataset --- Configuration/GlobalAssemblyInfo.cs | 2 +- Configuration/Version.defs | 4 +- CoreUtils/Utils.csproj | 2 + .../08_Datasets/06_LoadIris.pas | 16 + Release/pabcversion.txt | 2 +- ReleaseGenerators/Files/Datasets/iris.csv | 151 ++++++++ ReleaseGenerators/Files/Datasets/iris.meta | 34 ++ ReleaseGenerators/PascalABCNET_version.nsh | 2 +- ReleaseGenerators/RebuildStandartModules.pas | 2 +- .../RebuildStandartModulesMono.pas | 2 +- ReleaseGenerators/sect_Core.nsh | 6 + ReleaseGenerators/sect_Files.nsh | 4 + bin/Lib/DataAdapters.pas | 110 ++++++ bin/Lib/DataFrameABC.pas | 269 ++++++++++++- bin/Lib/MLABC.pas | 21 +- bin/Lib/MLDatasets.pas | 364 ++++++++++++++++-- bin/Lib/MLPipelineABC.pas | 47 +-- 17 files changed, 943 insertions(+), 95 deletions(-) create mode 100644 InstallerSamples/MachineLearning/08_Datasets/06_LoadIris.pas create mode 100644 ReleaseGenerators/Files/Datasets/iris.csv create mode 100644 ReleaseGenerators/Files/Datasets/iris.meta create mode 100644 bin/Lib/DataAdapters.pas diff --git a/Configuration/GlobalAssemblyInfo.cs b/Configuration/GlobalAssemblyInfo.cs index e7a245651..37a69999c 100644 --- a/Configuration/GlobalAssemblyInfo.cs +++ b/Configuration/GlobalAssemblyInfo.cs @@ -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 = "3773"; + public const string Revision = "3776"; public const string MainVersion = Major + "." + Minor; public const string FullVersion = Major + "." + Minor + "." + Build + "." + Revision; diff --git a/Configuration/Version.defs b/Configuration/Version.defs index b217f7047..561461942 100644 --- a/Configuration/Version.defs +++ b/Configuration/Version.defs @@ -1,4 +1,4 @@ -%MINOR%=11 -%REVISION%=3773 %COREVERSION%=1 +%REVISION%=3776 +%MINOR%=11 %MAJOR%=3 diff --git a/CoreUtils/Utils.csproj b/CoreUtils/Utils.csproj index 23625065b..886cfb913 100644 --- a/CoreUtils/Utils.csproj +++ b/CoreUtils/Utils.csproj @@ -7,5 +7,7 @@ false false + + $(OutputPath) \ No newline at end of file diff --git a/InstallerSamples/MachineLearning/08_Datasets/06_LoadIris.pas b/InstallerSamples/MachineLearning/08_Datasets/06_LoadIris.pas new file mode 100644 index 000000000..8f22f4a49 --- /dev/null +++ b/InstallerSamples/MachineLearning/08_Datasets/06_LoadIris.pas @@ -0,0 +1,16 @@ +uses MLABC; + +begin + var ds := Datasets.Iris; + + Println('Dataset: ', ds.Name); + Println('Task: ', ds.Task); + Println; + + Println('Rows:', ds.Data.RowCount); + Println('Features:', ds.Features.Length); + Println; + + Println('Head:'); + ds.Data.Print; +end. \ No newline at end of file diff --git a/Release/pabcversion.txt b/Release/pabcversion.txt index 5f7509154..bff0c8543 100644 --- a/Release/pabcversion.txt +++ b/Release/pabcversion.txt @@ -1 +1 @@ -3.11.1.3773 +3.11.1.3776 diff --git a/ReleaseGenerators/Files/Datasets/iris.csv b/ReleaseGenerators/Files/Datasets/iris.csv new file mode 100644 index 000000000..80511dba8 --- /dev/null +++ b/ReleaseGenerators/Files/Datasets/iris.csv @@ -0,0 +1,151 @@ +sepal_length,sepal_width,petal_length,petal_width,species +5.1,3.5,1.4,0.2,setosa +4.9,3.0,1.4,0.2,setosa +4.7,3.2,1.3,0.2,setosa +4.6,3.1,1.5,0.2,setosa +5.0,3.6,1.4,0.2,setosa +5.4,3.9,1.7,0.4,setosa +4.6,3.4,1.4,0.3,setosa +5.0,3.4,1.5,0.2,setosa +4.4,2.9,1.4,0.2,setosa +4.9,3.1,1.5,0.1,setosa +5.4,3.7,1.5,0.2,setosa +4.8,3.4,1.6,0.2,setosa +4.8,3.0,1.4,0.1,setosa +4.3,3.0,1.1,0.1,setosa +5.8,4.0,1.2,0.2,setosa +5.7,4.4,1.5,0.4,setosa +5.4,3.9,1.3,0.4,setosa +5.1,3.5,1.4,0.3,setosa +5.7,3.8,1.7,0.3,setosa +5.1,3.8,1.5,0.3,setosa +5.4,3.4,1.7,0.2,setosa +5.1,3.7,1.5,0.4,setosa +4.6,3.6,1.0,0.2,setosa +5.1,3.3,1.7,0.5,setosa +4.8,3.4,1.9,0.2,setosa +5.0,3.0,1.6,0.2,setosa +5.0,3.4,1.6,0.4,setosa +5.2,3.5,1.5,0.2,setosa +5.2,3.4,1.4,0.2,setosa +4.7,3.2,1.6,0.2,setosa +4.8,3.1,1.6,0.2,setosa +5.4,3.4,1.5,0.4,setosa +5.2,4.1,1.5,0.1,setosa +5.5,4.2,1.4,0.2,setosa +4.9,3.1,1.5,0.2,setosa +5.0,3.2,1.2,0.2,setosa +5.5,3.5,1.3,0.2,setosa +4.9,3.6,1.4,0.1,setosa +4.4,3.0,1.3,0.2,setosa +5.1,3.4,1.5,0.2,setosa +5.0,3.5,1.3,0.3,setosa +4.5,2.3,1.3,0.3,setosa +4.4,3.2,1.3,0.2,setosa +5.0,3.5,1.6,0.6,setosa +5.1,3.8,1.9,0.4,setosa +4.8,3.0,1.4,0.3,setosa +5.1,3.8,1.6,0.2,setosa +4.6,3.2,1.4,0.2,setosa +5.3,3.7,1.5,0.2,setosa +5.0,3.3,1.4,0.2,setosa +7.0,3.2,4.7,1.4,versicolor +6.4,3.2,4.5,1.5,versicolor +6.9,3.1,4.9,1.5,versicolor +5.5,2.3,4.0,1.3,versicolor +6.5,2.8,4.6,1.5,versicolor +5.7,2.8,4.5,1.3,versicolor +6.3,3.3,4.7,1.6,versicolor +4.9,2.4,3.3,1.0,versicolor +6.6,2.9,4.6,1.3,versicolor +5.2,2.7,3.9,1.4,versicolor +5.0,2.0,3.5,1.0,versicolor +5.9,3.0,4.2,1.5,versicolor +6.0,2.2,4.0,1.0,versicolor +6.1,2.9,4.7,1.4,versicolor +5.6,2.9,3.6,1.3,versicolor +6.7,3.1,4.4,1.4,versicolor +5.6,3.0,4.5,1.5,versicolor +5.8,2.7,4.1,1.0,versicolor +6.2,2.2,4.5,1.5,versicolor +5.6,2.5,3.9,1.1,versicolor +5.9,3.2,4.8,1.8,versicolor +6.1,2.8,4.0,1.3,versicolor +6.3,2.5,4.9,1.5,versicolor +6.1,2.8,4.7,1.2,versicolor +6.4,2.9,4.3,1.3,versicolor +6.6,3.0,4.4,1.4,versicolor +6.8,2.8,4.8,1.4,versicolor +6.7,3.0,5.0,1.7,versicolor +6.0,2.9,4.5,1.5,versicolor +5.7,2.6,3.5,1.0,versicolor +5.5,2.4,3.8,1.1,versicolor +5.5,2.4,3.7,1.0,versicolor +5.8,2.7,3.9,1.2,versicolor +6.0,2.7,5.1,1.6,versicolor +5.4,3.0,4.5,1.5,versicolor +6.0,3.4,4.5,1.6,versicolor +6.7,3.1,4.7,1.5,versicolor +6.3,2.3,4.4,1.3,versicolor +5.6,3.0,4.1,1.3,versicolor +5.5,2.5,4.0,1.3,versicolor +5.5,2.6,4.4,1.2,versicolor +6.1,3.0,4.6,1.4,versicolor +5.8,2.6,4.0,1.2,versicolor +5.0,2.3,3.3,1.0,versicolor +5.6,2.7,4.2,1.3,versicolor +5.7,3.0,4.2,1.2,versicolor +5.7,2.9,4.2,1.3,versicolor +6.2,2.9,4.3,1.3,versicolor +5.1,2.5,3.0,1.1,versicolor +5.7,2.8,4.1,1.3,versicolor +6.3,3.3,6.0,2.5,virginica +5.8,2.7,5.1,1.9,virginica +7.1,3.0,5.9,2.1,virginica +6.3,2.9,5.6,1.8,virginica +6.5,3.0,5.8,2.2,virginica +7.6,3.0,6.6,2.1,virginica +4.9,2.5,4.5,1.7,virginica +7.3,2.9,6.3,1.8,virginica +6.7,2.5,5.8,1.8,virginica +7.2,3.6,6.1,2.5,virginica +6.5,3.2,5.1,2.0,virginica +6.4,2.7,5.3,1.9,virginica +6.8,3.0,5.5,2.1,virginica +5.7,2.5,5.0,2.0,virginica +5.8,2.8,5.1,2.4,virginica +6.4,3.2,5.3,2.3,virginica +6.5,3.0,5.5,1.8,virginica +7.7,3.8,6.7,2.2,virginica +7.7,2.6,6.9,2.3,virginica +6.0,2.2,5.0,1.5,virginica +6.9,3.2,5.7,2.3,virginica +5.6,2.8,4.9,2.0,virginica +7.7,2.8,6.7,2.0,virginica +6.3,2.7,4.9,1.8,virginica +6.7,3.3,5.7,2.1,virginica +7.2,3.2,6.0,1.8,virginica +6.2,2.8,4.8,1.8,virginica +6.1,3.0,4.9,1.8,virginica +6.4,2.8,5.6,2.1,virginica +7.2,3.0,5.8,1.6,virginica +7.4,2.8,6.1,1.9,virginica +7.9,3.8,6.4,2.0,virginica +6.4,2.8,5.6,2.2,virginica +6.3,2.8,5.1,1.5,virginica +6.1,2.6,5.6,1.4,virginica +7.7,3.0,6.1,2.3,virginica +6.3,3.4,5.6,2.4,virginica +6.4,3.1,5.5,1.8,virginica +6.0,3.0,4.8,1.8,virginica +6.9,3.1,5.4,2.1,virginica +6.7,3.1,5.6,2.4,virginica +6.9,3.1,5.1,2.3,virginica +5.8,2.7,5.1,1.9,virginica +6.8,3.2,5.9,2.3,virginica +6.7,3.3,5.7,2.5,virginica +6.7,3.0,5.2,2.3,virginica +6.3,2.5,5.0,1.9,virginica +6.5,3.0,5.2,2.0,virginica +6.2,3.4,5.4,2.3,virginica +5.9,3.0,5.1,1.8,virginica \ No newline at end of file diff --git a/ReleaseGenerators/Files/Datasets/iris.meta b/ReleaseGenerators/Files/Datasets/iris.meta new file mode 100644 index 000000000..50170f442 --- /dev/null +++ b/ReleaseGenerators/Files/Datasets/iris.meta @@ -0,0 +1,34 @@ +name = Iris + +task = classification + +features = sepal_length,sepal_width,petal_length,petal_width +target = species + +description.en = The classic Fisher iris dataset. One of the most famous datasets in machine learning and statistics. +description.ru = Классический датасет ирисов Фишера. Один из самых известных датасетов в машинном обучении. + +source = UCI Machine Learning Repository +url = https://github.com/mwaskom/seaborn-data/blob/master/iris.csv + +feature.sepal_length = float +feature.sepal_width = float +feature.petal_length = float +feature.petal_width = float +feature.species = categorical + +feature.sepal_length.ru = длина чашелистика +feature.sepal_width.ru = ширина чашелистика +feature.petal_length.ru = длина лепестка +feature.petal_width.ru = ширина лепестка +feature.species.ru = вид ириса + +feature.sepal_length.en = sepal length +feature.sepal_width.en = sepal width +feature.petal_length.en = petal length +feature.petal_width.en = petal width +feature.species.en = iris species + +value.species.setosa.ru = ирис щетинистый +value.species.versicolor.ru = ирис разноцветный +value.species.virginica.ru = ирис виргинский \ No newline at end of file diff --git a/ReleaseGenerators/PascalABCNET_version.nsh b/ReleaseGenerators/PascalABCNET_version.nsh index 29d030df0..1fc727dbc 100644 --- a/ReleaseGenerators/PascalABCNET_version.nsh +++ b/ReleaseGenerators/PascalABCNET_version.nsh @@ -1 +1 @@ -!define VERSION '3.11.1.3773' +!define VERSION '3.11.1.3776' diff --git a/ReleaseGenerators/RebuildStandartModules.pas b/ReleaseGenerators/RebuildStandartModules.pas index b17339c17..04a6666d0 100644 --- a/ReleaseGenerators/RebuildStandartModules.pas +++ b/ReleaseGenerators/RebuildStandartModules.pas @@ -22,7 +22,7 @@ uses WPF, DataFrameABC, DataFrameABCCore, LinearAlgebraML, PreprocessorABC, MetricsABC, MLABC, MLCoreABC, MLModelsABC, ValidationML, MLExceptions, InspectionML, MLPipelineABC, - PlotML, MLDatasets + PlotML, MLDatasets, DataAdapters ; begin diff --git a/ReleaseGenerators/RebuildStandartModulesMono.pas b/ReleaseGenerators/RebuildStandartModulesMono.pas index af04ea0a4..a75e259fb 100644 --- a/ReleaseGenerators/RebuildStandartModulesMono.pas +++ b/ReleaseGenerators/RebuildStandartModulesMono.pas @@ -12,7 +12,7 @@ School, SF, TurtleABC, DataFrameABC, DataFrameABCCore, LinearAlgebraML, PreprocessorABC, MetricsABC, MLABC, MLCoreABC, MLModelsABC, ValidationML, MLExceptions, InspectionML, MLPipelineABC, - PlotML, MLDatasets + PlotML, MLDatasets, DataAdapters ; begin diff --git a/ReleaseGenerators/sect_Core.nsh b/ReleaseGenerators/sect_Core.nsh index fff471ac4..6e897bf72 100644 --- a/ReleaseGenerators/sect_Core.nsh +++ b/ReleaseGenerators/sect_Core.nsh @@ -193,6 +193,8 @@ File ..\bin\Lib\MLPipelineABC.pcu File ..\bin\Lib\PlotML.pcu File ..\bin\Lib\MLDatasets.pcu + File ..\bin\Lib\DataAdapters.pcu + File ..\bin\Lib\PABCRtl.dll File ..\bin\Lib\HelixToolkit.Wpf.dll @@ -300,6 +302,7 @@ ${AddFile} "MLPipelineABC.pcu" ${AddFile} "PlotML.pcu" ${AddFile} "MLDatasets.pcu" + ${AddFile} "DataAdapters.pcu" ${AddFile} "turtle.png" @@ -444,6 +447,7 @@ File ..\bin\Lib\MLPipelineABC.pas File ..\bin\Lib\PlotML.pas File ..\bin\Lib\MLDatasets.pas + File ..\bin\Lib\DataAdapters.pas File ..\bin\Lib\__RedirectIOMode.vb @@ -539,6 +543,8 @@ ${AddFile} "MLPipelineABC.pas" ${AddFile} "PlotML.pas" ${AddFile} "MLDatasets.pas" + ${AddFile} "DataAdapters.pas" + ${AddFile} "__RedirectIOMode.vb" ${AddFile} "VBSystem.vb" diff --git a/ReleaseGenerators/sect_Files.nsh b/ReleaseGenerators/sect_Files.nsh index d60126d3b..31e726169 100644 --- a/ReleaseGenerators/sect_Files.nsh +++ b/ReleaseGenerators/sect_Files.nsh @@ -6,6 +6,10 @@ File "Files\Databases\*.txt" File "Files\Words\*.txt" + SetOutPath "$INSTDIR\Files\Datasets" + File "Files\Datasets\*.csv" + File "Files\Datasets\*.meta" + SetOutPath "$INSTDIR\Files\Images" File "Files\Images\*.png" SectionEnd diff --git a/bin/Lib/DataAdapters.pas b/bin/Lib/DataAdapters.pas new file mode 100644 index 000000000..525885a94 --- /dev/null +++ b/bin/Lib/DataAdapters.pas @@ -0,0 +1,110 @@ +unit DataAdapters; + +interface + +uses DataFrameABC; +uses LinearAlgebraML; + +/// Преобразует вектор меток классов в массив целых чисел. +/// Используется при визуализации и других задачах, +/// где метки должны быть представлены как 0,1,2,... +/// Значения округляются функцией Round, чтобы устранить +/// возможные небольшие численные ошибки +function LabelsToInts(y: Vector): array of integer; + +function EncodeLabels(labels: array of string): array of integer; + +implementation + +uses MLExceptions; + +const + ER_TO_MATRIX_NO_COLUMNS = + 'ToMatrix: не указаны столбцы!!ToMatrix: no columns specified'; + ER_TO_VECTOR_NON_NUMERIC = + 'ToVector: столбец "{0}" содержит нечисловые или NA значения!!' + + 'ToVector: column "{0}" contains non-numeric or NA values'; + ER_TO_MATRIX_NON_NUMERIC = + 'Столбец "{0}" содержит нечисловые или NA значения!!Column "{0}" contains non-numeric or NA values'; + +function LabelsToInts(y: Vector): array of integer; +begin + if y = nil then + ArgumentNullError(ER_ARG_NULL, 'y'); + + Result := ArrGen(y.Length, i -> Round(y[i])); +end; + +function EncodeLabels(labels: array of string): array of integer; +begin + if labels = nil then + ArgumentNullError(ER_ARG_NULL, 'labels'); + + var map := new Dictionary; + var res := new integer[labels.Length]; + + var next := 0; + + for var i := 0 to labels.Length - 1 do + begin + var s := labels[i]; + + if not map.ContainsKey(s) then + begin + map[s] := next; + next += 1; + end; + + res[i] := map[s]; + end; + + Result := res; +end; + +function ToMatrix(Self: DataFrame; colNames: array of string): Matrix; extensionmethod; +begin + var df := Self; + var n := df.RowCount; + var p := colNames.Length; + + if p = 0 then + ArgumentError(ER_TO_MATRIX_NO_COLUMNS); + + Result := new Matrix(n, p); + + for var j := 0 to p - 1 do + begin + var col := df[colNames[j]]; + + for var i := 0 to n - 1 do + begin + var value: real; + + if not col.TryGetNumericValue(i, value) then + ArgumentError(ER_TO_MATRIX_NON_NUMERIC, colNames[j]); + + Result[i,j] := value; + end; + end; +end; + +function ToVector(Self: DataFrame; colName: string): Vector; extensionmethod; +begin + var df := Self; + var n := df.RowCount; + Result := new Vector(n); + + var col := df[colName]; + + for var i := 0 to n - 1 do + begin + var value: real; + + if not col.TryGetNumericValue(i, value) then + ArgumentError(ER_TO_VECTOR_NON_NUMERIC, colName); + + Result[i] := value; + end; +end; + +end. \ No newline at end of file diff --git a/bin/Lib/DataFrameABC.pas b/bin/Lib/DataFrameABC.pas index 999e7ee5c..7f40d5d16 100644 --- a/bin/Lib/DataFrameABC.pas +++ b/bin/Lib/DataFrameABC.pas @@ -114,9 +114,24 @@ type /// Возвращает индекс столбца по имени function ColumnIndex(name: string): integer; + function HasColumn(name: string): boolean; + /// Создает курсор для итерации по строкам function GetCursor: DataFrameCursor; + /// Разбивает таблицу на обучающую и тестовую выборки. + /// + /// Строки случайным образом перемешиваются и делятся на две части: + /// train и test. Доля тестовой выборки задаётся параметром testRatio. + /// + /// testRatio = 0.2 означает, что 20% строк попадут в тестовую выборку. + /// + /// Если seed >= 0, разбиение будет детерминированным. + /// Если seed = -1, используется случайная инициализация генератора. + /// + /// Возвращает кортеж (trainDataFrame, testDataFrame). + function TrainTestSplit(testRatio: real := 0.2; seed: integer := -1): (DataFrame, DataFrame); + /// Добавляет столбец целых чисел procedure AddIntColumn(name: string; data: array of integer; valid: array of boolean := nil; isCategorical: boolean := false); /// Добавляет столбец вещественных чисел @@ -130,6 +145,10 @@ type function GetIntColumn(name: string): array of integer; /// Возвращает данные вещественного столбца по имени function GetFloatColumn(name: string): array of real; + /// Возвращает данные строкового столбца по имени + function GetStrColumn(name: string): array of string; + /// Возвращает данные логического столбца по имени + function GetBoolColumn(name: string): array of boolean; /// Вычисляет сумму значений столбца по индексу function Sum(colIndex: integer): real; @@ -428,7 +447,9 @@ const ER_CSV_INVALID_BOOL = 'Некорректное логическое значение "{0}" в столбце "{1}"!!' + 'Invalid bool "{0}" in column "{1}"'; - + ER_TEST_RATIO_INVALID = + 'Некорректное значение testRatio = {0}. Ожидается число в диапазоне (0, 1).' + + 'Invalid testRatio = {0}. Expected value in range (0, 1).'; type /// Класс для группировки данных @@ -1478,6 +1499,11 @@ begin Error(ER_COLUMN_NOT_FOUND, name); end; +function DataFrame.HasColumn(name: string): boolean; +begin + Result := fSchema.HasColumn(name); +end; + function DataFrame.GetCursor: DataFrameCursor := new DataFrameCursor(columns.ToArray,fSchema); @@ -1495,6 +1521,67 @@ begin Result := c.Data; end; +function DataFrame.GetStrColumn(name: string): array of string; +begin + var i := ColumnIndex(name); + var c := StrColumn(columns[i]); + Result := c.Data; +end; + +function DataFrame.GetBoolColumn(name: string): array of boolean; +begin + var i := ColumnIndex(name); + var c := BoolColumn(columns[i]); + Result := c.Data; +end; + +function DataFrame.TrainTestSplit(testRatio: real; seed: integer): (DataFrame, DataFrame); +begin + if Self = nil then + ArgumentNullError(ER_ARG_NULL, 'DataFrame'); + + if (testRatio <= 0.0) or (testRatio >= 1.0) then + ArgumentError(ER_TEST_RATIO_INVALID, testRatio); + + var n := RowCount; + + if n < 2 then + ArgumentError(ER_EMPTY_DATA, 'TrainTestSplit'); + + var actualSeed := if seed >= 0 then seed else System.Environment.TickCount and integer.MaxValue; + var rnd := new System.Random(actualSeed); + + var idx := Arr(0..n-1); + idx.Shuffle(rnd); + + var rawSize := Round(n * testRatio); + var testSize := rawSize.Clamp(1, n - 1); + + // --- маркер тестовых строк + var isTest := new boolean[n]; + + for var i := 0 to testSize - 1 do + isTest[idx[i]] := true; + + var trainDf := new DataFrame; + var testDf := new DataFrame; + + var cur := GetCursor; + var row := 0; + + while cur.MoveNext do + begin + if isTest[row] then + testDf.AppendRowFromCursor(self, cur) + else + trainDf.AppendRowFromCursor(self, cur); + + row += 1; + end; + + Result := (trainDf, testDf); +end; + procedure DataFrame.AddIntColumn(name: string; data: array of integer; valid: array of boolean; isCategorical: boolean); begin if (columns.Count > 0) and (data.Length <> RowCount) then @@ -1531,7 +1618,7 @@ begin else begin if valid.Length <> data.Length then - DimensionError(ER_COLUMN_VALID_LENGTH_MISMATCH); + DimensionError(ER_COLUMN_VALID_LENGTH_MISMATCH); c.IsValid := valid; end; @@ -2835,7 +2922,7 @@ begin end; -procedure DataFrame.PrintPreview(maxRows: integer; headRows: integer; decimals: integer); +{procedure DataFrame.PrintPreview(maxRows: integer; headRows: integer; decimals: integer); begin var ColumnSeparator := ' '; var colCount := columns.Count; @@ -3025,6 +3112,179 @@ begin PABCSystem.Print(FormatValue(j) + ColumnSeparator); PABCSystem.Println; end; +end;} + +procedure DataFrame.PrintPreview(maxRows: integer; headRows: integer; decimals: integer); +begin + var colCount := columns.Count; + if colCount = 0 then exit; + + var rowCount := RowCount; + if rowCount = 0 then exit; + + if maxRows < 1 then exit; + if decimals < 0 then decimals := 0; + + if rowCount <= maxRows then + headRows := rowCount + else + begin + if headRows = -1 then + headRows := (maxRows + 1) div 2; + + if headRows < 0 then headRows := 0; + if headRows > maxRows then headRows := maxRows; + end; + + var tailRows := maxRows - headRows; + + if tailRows < 0 then + tailRows := 0; + + if tailRows > rowCount - headRows then + tailRows := rowCount - headRows; + + var widths := new integer[colCount]; + + // --- начальная ширина = длина заголовка --- + for var j := 0 to colCount - 1 do + begin + widths[j] := columns[j].Info.Name.Length; + if widths[j] < 2 then + widths[j] := 2; + end; + + var cursor := GetCursor; + + // --- scan head --- + for var i := 0 to headRows - 1 do + begin + cursor.MoveTo(i); + + for var j := 0 to colCount - 1 do + begin + var s: string; + + if not cursor.IsValid(j) then + s := 'NA' + else + case columns[j].Info.ColType of + ctInt: s := cursor.Int(j).ToString; + ctFloat: s := cursor.Float(j).ToString('F' + decimals); + ctStr: s := cursor.Str(j); + ctBool: s := cursor.Bool(j).ToString; + end; + + if s.Length > widths[j] then + widths[j] := s.Length; + end; + end; + + // --- scan tail --- + if rowCount > headRows then + for var i := rowCount - tailRows to rowCount - 1 do + if i >= headRows then + begin + cursor.MoveTo(i); + + for var j := 0 to colCount - 1 do + begin + var s: string; + + if not cursor.IsValid(j) then + s := 'NA' + else + case columns[j].Info.ColType of + ctInt: s := cursor.Int(j).ToString; + ctFloat: s := cursor.Float(j).ToString('F' + decimals); + ctStr: s := cursor.Str(j); + ctBool: s := cursor.Bool(j).ToString; + end; + + if s.Length > widths[j] then + widths[j] := s.Length; + end; + end; + + // --- header --- + for var j := 0 to colCount - 1 do + begin + PABCSystem.Print(columns[j].Info.Name.PadLeft(widths[j])); + if j < colCount - 1 then + Write(' '); + end; + + PABCSystem.Println; + + // --- head rows --- + for var i := 0 to headRows - 1 do + begin + cursor.MoveTo(i); + + for var j := 0 to colCount - 1 do + begin + var s: string; + + if not cursor.IsValid(j) then + s := 'NA' + else + case columns[j].Info.ColType of + ctInt: s := cursor.Int(j).ToString; + ctFloat: s := cursor.Float(j).ToString('F' + decimals); + ctStr: s := cursor.Str(j); + ctBool: s := cursor.Bool(j).ToString; + end; + + PABCSystem.Print(s.PadLeft(widths[j])); + + if j < colCount - 1 then + Write(' '); + end; + + PABCSystem.Println; + end; + + // --- ellipsis --- + if headRows + tailRows < rowCount then + begin + for var j := 0 to colCount - 1 do + begin + PABCSystem.Print($'…'.PadLeft(widths[j])); + if j < colCount - 1 then + Write(' '); + end; + + PABCSystem.Println; + end; + + // --- tail rows --- + for var i := rowCount - tailRows to rowCount - 1 do + if i >= headRows then + begin + cursor.MoveTo(i); + + for var j := 0 to colCount - 1 do + begin + var s: string; + + if not cursor.IsValid(j) then + s := 'NA' + else + case columns[j].Info.ColType of + ctInt: s := cursor.Int(j).ToString; + ctFloat: s := cursor.Float(j).ToString('F' + decimals); + ctStr: s := cursor.Str(j); + ctBool: s := cursor.Bool(j).ToString; + end; + + PABCSystem.Print(s.PadLeft(widths[j])); + + if j < colCount - 1 then + Write(' '); + end; + + PABCSystem.Println; + end; end; procedure DataFrame.PrintlnPreview(maxRows: integer; headRows: integer; decimals: integer); @@ -3035,7 +3295,7 @@ end; procedure DataFrame.Print(decimals: integer); begin - PrintPreview(20, 10, decimals); + PrintPreview(10, 5, decimals); end; procedure DataFrame.Println(decimals: integer); @@ -3934,7 +4194,6 @@ end; // CSVLoader //----------------------------- - {procedure ScanFields(line: string; delimiter: char; starts, lens: array of integer; var actualCount: integer); begin diff --git a/bin/Lib/MLABC.pas b/bin/Lib/MLABC.pas index f8fddbf6f..8cfd25551 100644 --- a/bin/Lib/MLABC.pas +++ b/bin/Lib/MLABC.pas @@ -15,6 +15,7 @@ uses MLExceptions; uses InspectionML; uses MLPipelineABC; uses MLDatasets; +uses DataAdapters; type Vector = LinearAlgebraML.Vector; @@ -79,21 +80,17 @@ type Datasets = MLDatasets.Datasets; -/// Преобразует вектор меток классов в массив целых чисел. -/// Используется при визуализации и других задачах, -/// где метки должны быть представлены как 0,1,2,... -/// Значения округляются функцией Round, чтобы устранить -/// возможные небольшие численные ошибки -function LabelsToInts(y: Vector): array of integer; + function LabelsToInts(y: Vector): array of integer; + function EncodeLabels(labels: array of string): array of integer; + implementation - + function LabelsToInts(y: Vector): array of integer; begin - if y = nil then - ArgumentNullError(ER_ARG_NULL, 'y'); - - Result := ArrGen(y.Length, i -> Round(y[i])); + Result := DataAdapters.LabelsToInts(y); end; - + +function EncodeLabels(labels: array of string): array of integer := DataAdapters.EncodeLabels(labels); + end. \ No newline at end of file diff --git a/bin/Lib/MLDatasets.pas b/bin/Lib/MLDatasets.pas index 301f5546f..e32767a9c 100644 --- a/bin/Lib/MLDatasets.pas +++ b/bin/Lib/MLDatasets.pas @@ -5,6 +5,56 @@ interface uses DataFrameABC, LinearAlgebraML; type + TaskType = (Classification, Regression, Clustering); + + /// ML-датасет: таблица данных вместе с метаданными задачи. + /// + /// Dataset содержит: + /// Data — таблицу данных (DataFrame) + /// Features — список признаков + /// Target — целевую переменную (если есть) + /// Task — тип задачи (classification/regression/clustering) + /// + /// Предоставляет удобные методы для получения матриц признаков + /// и целевых значений для обучения моделей + Dataset = class + public + Name: string; + Data: DataFrame; + Features: array of string; + Target: string; + Task: TaskType; + + /// Возвращает матрицу признаков X. + /// Используются столбцы, указанные в Features. + function ToX: Matrix; + + /// Возвращает вектор целевой переменной y. + /// Доступно только для задач с учителем. + function ToY: Vector; + + /// Возвращает пару (X, y) для обучения модели. + function ToXY: (Matrix, Vector); + + /// Возвращает (X, y), где y преобразован в целочисленные метки классов. + /// Используется для задач классификации. + function ToXYInt: (Matrix, array of integer); + + /// Возвращает true, если датасет относится к задаче с учителем + /// (classification или regression). + function IsSupervised: boolean; + + /// Разбивает датасет на обучающую и тестовую части. + /// testRatio — доля тестовой выборки. + function TrainTestSplit(testRatio: real := 0.2; seed: integer := -1): (Dataset, Dataset); + + /// Возвращает первые n строк таблицы данных. + function Head(n: integer := 5): DataFrame; + + /// Возвращает краткое описание датасета (метаданные). + function Describe: DataFrame; + end; + /// Набор генераторов и загрузчиков датасетов для задач машинного обучения. /// Содержит синтетические генераторы (MakeBlobs, MakeMoons, MakeRegression) /// и реальные учебные датасеты (например, RussianHousing, StudentExam). @@ -78,7 +128,7 @@ type /// • noise — стандартное отклонение гауссовского шума, добавляемого к координатам. /// • factor — отношение радиуса внутреннего круга к внешнему (0 < factor < 1). /// • shuffle — перемешивать ли порядок объектов. - /// • seed — значение генератора случайных чисел (-1 означает использовать текущее время). + /// • seed — значение генератора случайных чисел (-1 означает использовать текущее время) static function MakeCircles( n: integer := 300; noise: real := 0.05; @@ -96,7 +146,7 @@ type /// • turns — число оборотов спирали. /// • radius — максимальный радиус спирали. /// • shuffle — перемешивать ли порядок объектов. - /// • seed — значение генератора случайных чисел (-1 означает использовать текущее время). + /// • seed — значение генератора случайных чисел (-1 означает использовать текущее время) static function MakeSpiral( n: integer := 300; classes: integer := 2; @@ -106,31 +156,67 @@ type shuffle: boolean := true; seed: integer := -1): (Matrix, Vector); + /// Загружает датасет по имени. + /// + /// Датасет ищется в каталоге: + /// Files\Datasets + /// + /// Для датасета должны существовать файлы: + /// name.meta — метаданные датасета + /// name.csv — данные таблицы + /// + /// Метаданные определяют: + /// тип задачи (classification / regression / clustering) + /// целевую переменную + /// список признаков (опционально) + /// + /// Если признаки не указаны в .meta, используются + /// все столбцы таблицы, кроме целевой переменной. + /// + /// Пример: + /// var ds := Datasets.Load('Iris'); + static function Load(name: string): Dataset; + // --- DataFrame датасеты (реалистичные таблицы, считываемые из csv) + /// Датасет Iris (классификация). + /// + /// 150 объектов, 4 признака: + /// sepal_length, sepal_width, petal_length, petal_width. + /// Целевая переменная: species. + /// + /// Пример: + /// var ds := Datasets.Iris; + /// var (X,y) := ds.ToXYInt; + static function Iris: Dataset; + /// Датасет цен на квартиры (задача регрессии) - static function RussianHousing: DataFrame; + static function RussianHousing: Dataset; /// Датасет результатов экзамена студентов (классификация) - static function StudentExam: DataFrame; + static function StudentExam: Dataset; /// Датасет банковских клиентов (классификация одобрения кредита) - static function BankClients: DataFrame; + static function BankClients: Dataset; /// Датасет поездок такси (регрессия стоимости поездки) - static function TaxiTrips: DataFrame; + static function TaxiTrips: Dataset; /// Датасет транспортной активности пассажиров (кластеризация) - static function MoscowTransport: DataFrame; + static function MoscowTransport: Dataset; /// Датасет интернет-покупок пользователей (классификация покупки) - static function OnlineShopping: DataFrame; - + static function OnlineShopping: Dataset; + + static function LoadMeta(path: string): Dictionary; + static function ParseFeatures(meta: Dictionary): array of string; end; + implementation uses MLExceptions; +uses DataAdapters; const ER_PARAM_GT_ZERO = @@ -141,7 +227,25 @@ const 'Параметр {0} должен быть >= 0!!Parameter {0} must be >= 0'; ER_PARAM_BETWEEN_01 = 'Параметр {0} должен быть в диапазоне (0,1)!!Parameter {0} must be in range (0,1)'; - + ER_DATASET_NO_TARGET = + 'У датасета нет целевой переменной (задача кластеризации).' + + 'Dataset has no target variable (clustering task).'; + ER_DATASET_TARGET_NOT_FOUND = + 'Целевая переменная "{0}" не найдена в таблице.' + + 'Target column "{0}" not found in DataFrame'; + ER_DATASET_META_NOT_FOUND = + 'Файл метаданных датасета "{0}" не найден!!Dataset meta file "{0}" not found'; + ER_DATASET_CSV_NOT_FOUND = + 'Файл данных датасета "{0}" не найден!!Dataset csv file "{0}" not found'; + ER_DATASET_TASK_UNKNOWN = + 'Неизвестный тип задачи датасета "{0}"!!Unknown dataset task "{0}"'; + ER_DATASET_TASK_MISSING = + 'В метаданных датасета "{0}" отсутствует поле task!!Dataset meta missing field "task" for "{0}"'; + ER_DATASET_FEATURE_NOT_FOUND = + 'Признак "{0}" не найден в датасете!!Feature column "{0}" not found in dataset'; + ER_DATASET_FEATURE_EQUALS_TARGET = + 'Признак "{0}" совпадает с целевой переменной!!Feature "{0}" equals target column'; + function Normal(rnd: System.Random): real; begin var u1 := rnd.NextDouble; @@ -149,15 +253,116 @@ begin Result := Sqrt(-2 * Ln(u1)) * Cos(2 * Pi * u2); end; -/// Генерирует синтетический датасет из нескольких гауссовых кластеров. -/// Возвращает матрицу признаков X и вектор меток кластеров y. -/// -/// Параметры: -/// • n — количество объектов (точек) -/// • centers — число кластеров -/// • clusterStd — стандартное отклонение точек внутри кластера -/// • nFeatures — число признаков (размерность пространства) -/// • seed — значение генератора случайных чисел (seed < 0 → случайный) +//----------------------------- +// Dataset +//----------------------------- + +function Dataset.ToX(): Matrix; +begin + Result := Data.ToMatrix(Features); +end; + +function Dataset.ToY(): Vector; +begin + if Task = Clustering then + ArgumentError(ER_DATASET_NO_TARGET); + + Result := Data.ToVector(Target); +end; + +function Dataset.ToXY(): (Matrix, Vector); +begin + Result := (ToX, ToY); +end; + +function Dataset.ToXYInt: (Matrix, array of integer); +begin + if Task <> Classification then + ArgumentError(ER_DATASET_NO_TARGET); + + var X := ToX; + + var labels := Data.GetStrColumn(Target); + var y := EncodeLabels(labels); + + Result := (X, y); +end; + +function Dataset.IsSupervised: boolean; +begin + Result := Task <> Clustering; +end; + +function Dataset.TrainTestSplit(testRatio: real; seed: integer): (Dataset, Dataset); +begin + if Self = nil then + ArgumentNullError(ER_ARG_NULL, 'Dataset'); + + var (trainDf, testDf) := Data.TrainTestSplit(testRatio, seed); + + var trainDs := new Dataset; + trainDs.Name := Name; + trainDs.Data := trainDf; + trainDs.Features := Features; + trainDs.Target := Target; + trainDs.Task := Task; + + var testDs := new Dataset; + testDs.Name := Name; + testDs.Data := testDf; + testDs.Features := Features; + testDs.Target := Target; + testDs.Task := Task; + + Result := (trainDs, testDs); +end; + +function Dataset.Head(n: integer): DataFrame; +begin + if Data = nil then + ArgumentNullError(ER_ARG_NULL, 'Data'); + + Result := Data.Head(n); +end; + +function Dataset.Describe: DataFrame; +begin + if Data = nil then + ArgumentNullError(ER_ARG_NULL, 'Data'); + + var props := Arr( + 'Name', + 'Rows', + 'Columns', + 'Task', + 'Target', + 'FeatureCount', + 'Features' + ); + + var values := Arr( + Name, + Data.RowCount.ToString, + Data.ColumnCount.ToString, + Task.ToString, + Target, + Features.Length.ToString, + Features.JoinToString(', ') + ); + + var valid := ArrFill(props.Length, true); + + var df := new DataFrame; + df.AddStrColumn('Property', props, valid, true); + df.AddStrColumn('Value', values, valid, false); + + Result := df; +end; + +//----------------------------- +// Datasets +//----------------------------- + static function Datasets.MakeBlobs( n: integer; centers: integer; clusterStd: real; nFeatures: integer; @@ -454,35 +659,144 @@ begin Result := (X, y); end; -static function Datasets.RussianHousing: DataFrame; +function ParseTask(s: string): TaskType; +begin + case s.ToLower of + 'classification': Result := Classification; + 'regression': Result := Regression; + 'clustering': Result := Clustering; + else + ArgumentError(ER_DATASET_TASK_UNKNOWN, s); + end; +end; + +static function Datasets.Load(name: string): Dataset; +begin + if name = nil then + ArgumentNullError(ER_ARG_NULL, 'name'); + + var baseDir := PascalABCDirectory + 'Files\Datasets\'; + + var metaPath := baseDir + name + '.meta'; + var csvPath := baseDir + name + '.csv'; + + if not FileExists(metaPath) then + ArgumentError(ER_DATASET_META_NOT_FOUND, name); + + if not FileExists(csvPath) then + ArgumentError(ER_DATASET_CSV_NOT_FOUND, name); + + var meta := LoadMeta(metaPath); + + if not meta.ContainsKey('task') then + ArgumentError(ER_DATASET_TASK_MISSING, name); + + var df := DataFrame.FromCsv(csvPath); + + var ds := new Dataset; + + ds.Name := name; + ds.Data := df; + + ds.Task := ParseTask(meta['task']); + ds.Target := meta['target']; + ds.Features := ParseFeatures(meta); + + // --- проверка target + if (ds.Target <> nil) and not df.HasColumn(ds.Target) then + ArgumentError(ER_DATASET_TARGET_NOT_FOUND, ds.Target); + + // --- проверка features + foreach var f in ds.Features do + begin + if not df.HasColumn(f) then + ArgumentError(ER_DATASET_FEATURE_NOT_FOUND, f); + + if (ds.Target <> nil) and (f = ds.Target) then + ArgumentError(ER_DATASET_FEATURE_EQUALS_TARGET, f); + end; + Result := ds; +end; + +static function Datasets.Iris: Dataset; +begin + Result := Load('Iris'); +end; + +static function Datasets.RussianHousing: Dataset; begin Result := nil; end; -static function Datasets.StudentExam: DataFrame; +static function Datasets.StudentExam: Dataset; begin Result := nil; end; -static function Datasets.BankClients: DataFrame; +static function Datasets.BankClients: Dataset; begin Result := nil; end; -static function Datasets.TaxiTrips: DataFrame; +static function Datasets.TaxiTrips: Dataset; begin Result := nil; end; -static function Datasets.MoscowTransport: DataFrame; +static function Datasets.MoscowTransport: Dataset; begin Result := nil; end; -static function Datasets.OnlineShopping: DataFrame; +static function Datasets.OnlineShopping: Dataset; begin Result := nil; end; +static function Datasets.LoadMeta(path: string): Dictionary; +begin + if path = nil then + ArgumentNullError(ER_ARG_NULL, 'path'); + + if not FileExists(path) then + ArgumentError(ER_DATASET_META_NOT_FOUND, path); + + var dict := new Dictionary; + + foreach var line in ReadLines(path) do + begin + var s := line.Trim; + + if s = '' then + continue; + + if s.StartsWith('#') then + continue; + + var p := s.IndexOf('='); + + if p < 0 then + continue; + + var key := s.Substring(0,p).Trim; + var val := s.Substring(p+1).Trim; + + dict[key] := val; + end; + + Result := dict; +end; + +static function Datasets.ParseFeatures(meta: Dictionary): array of string; +begin + if not meta.ContainsKey('features') then + exit(nil); + + Result := + meta['features'] + .Split(',') + .Select(s -> s.Trim) + .ToArray; +end; end. \ No newline at end of file diff --git a/bin/Lib/MLPipelineABC.pas b/bin/Lib/MLPipelineABC.pas index 32f3103c4..d3050e1eb 100644 --- a/bin/Lib/MLPipelineABC.pas +++ b/bin/Lib/MLPipelineABC.pas @@ -161,6 +161,7 @@ type implementation uses MLExceptions; +uses DataAdapters; const ER_PIPELINE_MODIFY_AFTER_FIT = @@ -202,52 +203,6 @@ const ER_DATAPIPE_DUPLICATE_FEATURE = 'Повторяющийся признак: {0}!!Duplicate feature: {0}'; -function ToMatrix(Self: DataFrame; colNames: array of string): Matrix; extensionmethod; -begin - var df := Self; - var n := df.RowCount; - var p := colNames.Length; - - if p = 0 then - ArgumentError(ER_TO_MATRIX_NO_COLUMNS); - - Result := new Matrix(n, p); - - for var j := 0 to p - 1 do - begin - var col := df[colNames[j]]; - - for var i := 0 to n - 1 do - begin - var value: real; - - if not col.TryGetNumericValue(i, value) then - ArgumentError(ER_TO_MATRIX_NON_NUMERIC, colNames[j]); - - Result[i,j] := value; - end; - end; -end; - -function ToVector(Self: DataFrame; colName: string): Vector; extensionmethod; -begin - var df := Self; - var n := df.RowCount; - Result := new Vector(n); - - var col := df[colName]; - - for var i := 0 to n - 1 do - begin - var value: real; - - if not col.TryGetNumericValue(i, value) then - ArgumentError(ER_TO_VECTOR_NON_NUMERIC, colName); - - Result[i] := value; - end; -end; - //----------------------------- // DataPipeline //-----------------------------