diff --git a/Configuration/GlobalAssemblyInfo.cs b/Configuration/GlobalAssemblyInfo.cs index 47979a029..d0576404b 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 = "3828"; + public const string Revision = "3830"; 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 8bc08b486..416c9680e 100644 --- a/Configuration/Version.defs +++ b/Configuration/Version.defs @@ -1,4 +1,4 @@ %COREVERSION%=1 -%REVISION%=3828 +%REVISION%=3830 %MINOR%=11 %MAJOR%=3 diff --git a/InstallerSamples/MachineLearning/02_DataFrame/df22.pas b/InstallerSamples/MachineLearning/02_DataFrame/df22.pas new file mode 100644 index 000000000..f5a4fb7f1 --- /dev/null +++ b/InstallerSamples/MachineLearning/02_DataFrame/df22.pas @@ -0,0 +1,18 @@ +uses DataFrameABC; + +begin + var df1 := DataFrame.FromCsvText(''' +id,name,age +1,Ann,10 +2,Bob,12 +'''); + + var df2 := DataFrame.FromCsvText(''' +id,name,age +3,Cat,11 +4,Dan,13 +'''); + + var df := DataFrame.Concat(df1, df2); + df.Print; +end. diff --git a/Release/pabcversion.txt b/Release/pabcversion.txt index 1f40492be..ef7dcaed1 100644 --- a/Release/pabcversion.txt +++ b/Release/pabcversion.txt @@ -1 +1 @@ -3.11.1.3828 +3.11.1.3830 diff --git a/ReleaseGenerators/PascalABCNET_version.nsh b/ReleaseGenerators/PascalABCNET_version.nsh index eea506f5e..272caafca 100644 --- a/ReleaseGenerators/PascalABCNET_version.nsh +++ b/ReleaseGenerators/PascalABCNET_version.nsh @@ -1 +1 @@ -!define VERSION '3.11.1.3828' +!define VERSION '3.11.1.3830' diff --git a/bin/Lib/DataFrameABC.pas b/bin/Lib/DataFrameABC.pas index 28bb07542..7ad7d8bb5 100644 --- a/bin/Lib/DataFrameABC.pas +++ b/bin/Lib/DataFrameABC.pas @@ -370,6 +370,12 @@ type function Join(other: DataFrame; keys: array of string; kind: JoinKind := jkInner): DataFrame; /// Соединяет с другим DataFrame по разным именам ключей function Join(other: DataFrame; leftKeys, rightKeys: array of string; kind: JoinKind := jkInner): DataFrame; + + /// Склеивает несколько DataFrame по строкам. + /// Все таблицы должны иметь одинаковую схему: + /// имена столбцов, их порядок, типы и categorical-флаги должны совпадать. + /// Возвращает новый DataFrame. + static function Concat(params dfs: array of DataFrame): DataFrame; /// Выводит DataFrame с настраиваемым числом строк procedure Print(maxRows: integer := 10; headRows: integer := -1; decimals: integer := 2); @@ -618,6 +624,13 @@ const 'CSV format error: expected {0} columns, got {1}'; ER_EMPTY_CSV = 'CSV-файл пуст!!Empty CSV'; + ER_CONCAT_EMPTY = + 'DataFrame.Concat требует хотя бы один DataFrame!!DataFrame.Concat requires at least one DataFrame'; + ER_CONCAT_DF_NULL = + 'DataFrame.Concat не принимает nil-таблицы!!DataFrame.Concat does not accept nil dataframes'; + ER_CONCAT_SCHEMA_MISMATCH = + 'DataFrame.Concat требует одинаковую схему: имена, порядок, типы и categorical-флаги столбцов должны совпадать!!' + + 'DataFrame.Concat requires identical schema: column names, order, types, and categorical flags must match'; ER_CSV_UNCLOSED_QUOTE = 'Ошибка формата CSV: незакрытая кавычка!!CSV format error: unclosed quote'; ER_CSV_INVALID_BOOL = @@ -4455,6 +4468,129 @@ begin Result := CsvLoader.LoadFromLines(text.ToLines); end; +static function DataFrame.Concat(params dfs: array of DataFrame): DataFrame; +begin + if (dfs = nil) or (dfs.Length = 0) then + ArgumentError(ER_CONCAT_EMPTY); + + for var i := 0 to dfs.Length - 1 do + if dfs[i] = nil then + ArgumentError(ER_CONCAT_DF_NULL); + + var first := dfs[0]; + var schema := first.fSchema; + + for var di := 1 to dfs.Length - 1 do + begin + var curSchema := dfs[di].fSchema; + + if curSchema.ColumnCount <> schema.ColumnCount then + ArgumentError(ER_CONCAT_SCHEMA_MISMATCH); + + for var ci := 0 to schema.ColumnCount - 1 do + if (curSchema.NameAt(ci) <> schema.NameAt(ci)) or + (curSchema.ColumnTypeAt(ci) <> schema.ColumnTypeAt(ci)) or + (curSchema.IsCategoricalAt(ci) <> schema.IsCategoricalAt(ci)) then + ArgumentError(ER_CONCAT_SCHEMA_MISMATCH); + end; + + var totalRows := 0; + for var di := 0 to dfs.Length - 1 do + totalRows += dfs[di].RowCount; + + var res := new DataFrame; + + for var ci := 0 to schema.ColumnCount - 1 do + begin + var name := schema.NameAt(ci); + + case schema.ColumnTypeAt(ci) of + ctInt: + begin + var data := new integer[totalRows]; + var valid := new boolean[totalRows]; + var pos := 0; + + for var di := 0 to dfs.Length - 1 do + begin + var col := IntColumn(dfs[di].columns[ci]); + for var r := 0 to col.Data.Length - 1 do + begin + data[pos] := col.Data[r]; + valid[pos] := col.IsValid[r]; + pos += 1; + end; + end; + + res.AddIntColumn(name, data, valid); + end; + + ctFloat: + begin + var data := new real[totalRows]; + var valid := new boolean[totalRows]; + var pos := 0; + + for var di := 0 to dfs.Length - 1 do + begin + var col := FloatColumn(dfs[di].columns[ci]); + for var r := 0 to col.Data.Length - 1 do + begin + data[pos] := col.Data[r]; + valid[pos] := col.IsValid[r]; + pos += 1; + end; + end; + + res.AddFloatColumn(name, data, valid); + end; + + ctStr: + begin + var data := new string[totalRows]; + var valid := new boolean[totalRows]; + var pos := 0; + + for var di := 0 to dfs.Length - 1 do + begin + var col := StrColumn(dfs[di].columns[ci]); + for var r := 0 to col.Data.Length - 1 do + begin + data[pos] := col.Data[r]; + valid[pos] := col.IsValid[r]; + pos += 1; + end; + end; + + res.AddStrColumn(name, data, valid); + end; + + ctBool: + begin + var data := new boolean[totalRows]; + var valid := new boolean[totalRows]; + var pos := 0; + + for var di := 0 to dfs.Length - 1 do + begin + var col := BoolColumn(dfs[di].columns[ci]); + for var r := 0 to col.Data.Length - 1 do + begin + data[pos] := col.Data[r]; + valid[pos] := col.IsValid[r]; + pos += 1; + end; + end; + + res.AddBoolColumn(name, data, valid); + end; + end; + end; + + res.SetSchema(schema); + Result := res; +end; + procedure DataFrame.ToCsv(filename: string); begin CsvSaver.Save(self, filename, ',', true); diff --git a/bin/Lib/MLABC.pas b/bin/Lib/MLABC.pas index 273cb05d8..14f2666ee 100644 --- a/bin/Lib/MLABC.pas +++ b/bin/Lib/MLABC.pas @@ -44,18 +44,29 @@ unit MLABC; // ПОЛИТИКА PREDICT // // 1. Классификация: -// • Predict возвращает метки классов в том же виде, -// в каком они были поданы модели при обучении. +// • Для матричных моделей Predict возвращает метки классов +// в том виде, в каком они были поданы модели при обучении. +// +// • ClassificationMatrixPipeline следует той же политике: +// Predict возвращает метки классов в том виде, +// в каком они были поданы вложенной модели при обучении. +// +// • ClassificationDataPipeline работает с DataFrame, где целевой столбец +// часто является строковым. Поэтому внутри pipeline целевой столбец +// кодируется в целые числа, и Predict возвращает именно эти коды. +// // • PredictLabels возвращает строковые метки классов. // +// • Таким образом, различие естественно: +// матричные модели и ClassificationMatrixPipeline возвращают метки +// из пространства обучения модели, а ClassificationDataPipeline +// возвращает коды, построенные при кодировании целевого столбца. +// // 2. Регрессия: // • Predict возвращает вектор числовых предсказаний. // // 3. Кластеризация: // • Predict и FitPredict возвращают номера кластеров. -// -// Внутреннее кодирование классов может использоваться внутри модели, -// но наружу через Predict оно не выдаётся. // ============================================================= interface diff --git a/bin/Lib/MLModelsABC.pas b/bin/Lib/MLModelsABC.pas index 038cd899d..6dd9a955a 100644 --- a/bin/Lib/MLModelsABC.pas +++ b/bin/Lib/MLModelsABC.pas @@ -7116,6 +7116,9 @@ function GradientBoostingClassifier.PredictStage(X: Matrix; m: integer): array o begin var probs := PredictStageProba(X, m); + if fLabels = nil then + ArgumentError(ER_CLASSES_NOT_AVAILABLE); + var n := probs.RowCount; SetLength(Result, n); @@ -7131,7 +7134,7 @@ begin best := cls; end; - Result[i] := fClasses[best]; + Result[i] := fLabels.ClassValueAt(best); end; end; diff --git a/bin/Lib/MLPipelineABC.pas b/bin/Lib/MLPipelineABC.pas index d3f57cfc3..11c30097f 100644 --- a/bin/Lib/MLPipelineABC.pas +++ b/bin/Lib/MLPipelineABC.pas @@ -879,21 +879,23 @@ end; function ClassificationDataPipeline.PredictLabels(df: DataFrame): array of string; begin + if df = nil then + ArgumentNullError(ER_ARG_NULL, 'df'); if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); - var encoded := Predict(df); - var classes := GetClassLabels; - Result := new string[encoded.Length]; - - for var i := 0 to encoded.Length - 1 do - begin - var idx := encoded[i]; - if (idx < 0) or (idx >= classes.Length) then - Error(ER_LABEL_INDEX_OUT_OF_RANGE, idx, classes.Length); - - Result[i] := classes[idx]; - end; + if fModel = nil then + ArgumentError(ER_MODEL_NULL); + + if not (fModel is IClassifier) then + Error(ER_PREDICT_NOT_SUPPORTED); + + var current := Transform(df); + + var X := current.ToMatrix(fFinalFeatures); + X := TransformMatrix(X); + + Result := (fModel as IClassifier).PredictLabels(X); end; function ClassificationDataPipeline.PredictProba(df: DataFrame): Matrix;