diff --git a/bin/Lib/DataFrameABC.pas b/bin/Lib/DataFrameABC.pas index fd59ecfba..defb25c4f 100644 --- a/bin/Lib/DataFrameABC.pas +++ b/bin/Lib/DataFrameABC.pas @@ -115,9 +115,10 @@ type function GetColumn(i: integer): Column; - /// Добавляет в DataFrame столбец-представление (view), - /// использующий те же данные, что и исходный столбец - procedure AddColumnView(src: Column); + /// Добавляет в DataFrame столбец-синоним, + /// разделяющий те же массивы данных и валидности с исходным столбцом. + /// Изменение данных повлияет на все DataFrame, использующие этот столбец. + procedure AddColumnAlias(src: Column); function ExtendSchema(name: string; colType: ColumnType; isCategorical: boolean): DataFrameSchema; @@ -164,12 +165,16 @@ type procedure AddBoolColumn(name: string; data: array of boolean; valid: array of boolean := nil); /// Возвращает данные целочисленного столбца по имени + /// Не изменяйте их, иначе DataFrame будет повреждён function GetIntColumn(name: string): array of integer; /// Возвращает данные вещественного столбца по имени + /// Не изменяйте их, иначе DataFrame будет повреждён function GetFloatColumn(name: string): array of real; /// Возвращает данные строкового столбца по имени + /// Не изменяйте их, иначе DataFrame будет повреждён function GetStrColumn(name: string): array of string; /// Возвращает данные логического столбца по имени + /// Не изменяйте их, иначе DataFrame будет повреждён function GetBoolColumn(name: string): array of boolean; /// Вычисляет сумму значений столбца по индексу @@ -224,13 +229,24 @@ type /// Возвращает статистику по всем числовым столбцам function DescribeAll: Dictionary; - /// Группирует данные по столбцу по индексу + /// Группирует данные по столбцу по индексу. + /// Поддерживаемые типы ключей: integer, string, boolean. + /// Вещественные столбцы (float) не поддерживаются из-за численной нестабильности. function GroupBy(colIndex: integer): IGroupByContext; - /// Группирует данные по столбцу по имени + + /// Группирует данные по столбцу по имени. + /// Поддерживаемые типы ключей: integer, string, boolean. + /// Вещественные столбцы (float) не поддерживаются из-за численной нестабильности. function GroupBy(colName: string): IGroupByContext; - /// Группирует данные по нескольким столбцам по индексам + + /// Группирует данные по нескольким столбцам по индексам. + /// Все столбцы-ключи должны иметь тип integer, string или boolean. + /// Использование float-столбцов в качестве ключей не поддерживается. function GroupBy(colIndices: array of integer): IGroupByContext; - /// Группирует данные по нескольким столбцам по именам + + /// Группирует данные по нескольким столбцам по именам. + /// Все столбцы-ключи должны иметь тип integer, string или boolean. + /// Использование float-столбцов в качестве ключей не поддерживается. function GroupBy(colNames: array of string): IGroupByContext; /// Возвращает первые n строк @@ -585,9 +601,11 @@ const 'Индекс строки вне диапазона!!Row index is out of range'; ER_UNSUPPORTED_COLUMN_TYPE = 'Неподдерживаемый тип столбца!!Unsupported column type'; - + ER_JOIN_FLOAT_KEY_NOT_SUPPORTED = + 'Соединение по вещественным ключам не поддерживается из-за ошибок точности!!Join on float keys is not supported due to precision issues'; + type - GroupKey = class + GroupKey = class(IComparable) private fValues: array of object; public @@ -596,6 +614,8 @@ type function GetHashCode: integer; override; property Values: array of object read fValues; + + function CompareTo(other: GroupKey): integer; end; /// Класс для группировки данных @@ -1155,7 +1175,10 @@ end; function DataFrame.LeftJoinSingleKeyFloat(other: DataFrame; leftKey, rightKey: integer): DataFrame; begin - var index := new Dictionary>; + Error(ER_JOIN_FLOAT_KEY_NOT_SUPPORTED); + Result := nil; + + {var index := new Dictionary>; var rcur := other.GetCursor; while rcur.MoveNext do @@ -1203,7 +1226,7 @@ begin end; end; - Result := BuildLeftJoinResult(Self, other, leftIdx, rightIdx, leftKey, rightKey); + Result := BuildLeftJoinResult(Self, other, leftIdx, rightIdx, leftKey, rightKey);} end; function DataFrame.LeftJoinSingleKeyStr(other: DataFrame; leftKey, rightKey: integer): DataFrame; @@ -1798,7 +1821,9 @@ end; function DataFrame.JoinInnerSingleKeyFloat(other: DataFrame; leftKey, rightKey: integer): DataFrame; begin - var index := new Dictionary>; + Error(ER_JOIN_FLOAT_KEY_NOT_SUPPORTED); + Result := nil; + {var index := new Dictionary>; var rcur := other.GetCursor; while rcur.MoveNext do @@ -1834,7 +1859,7 @@ begin end; end; - Result := BuildJoinResult(Self, other, leftIdx, rightIdx, leftKey, rightKey); + Result := BuildJoinResult(Self, other, leftIdx, rightIdx, leftKey, rightKey);} end; function DataFrame.JoinInnerSingleKeyStr(other: DataFrame; leftKey, rightKey: integer): DataFrame; @@ -2057,7 +2082,7 @@ begin Result := Self.Join(tmp, leftKeys, kind); - AssertSchemaConsistent; + Result.AssertSchemaConsistent; end; procedure DataFrame.RebuildSchema; @@ -2536,7 +2561,9 @@ begin acc += d * d; end; - var variance := acc / cnt; + var variance := + if cnt > 1 then acc / (cnt - 1) + else 0.0; Result := (mean, variance); end; @@ -2601,7 +2628,12 @@ begin Result.Count := cnt; Result.Mean := mean; - Result.Std := Sqrt(acc / cnt); + + if cnt > 1 then + Result.Std := Sqrt(acc / (cnt - 1)) + else + Result.Std := 0.0; + Result.Min := mn; Result.Max := mx; end; @@ -2778,7 +2810,7 @@ begin var cmp: integer; case columns[colIndices[i]].Info.ColType of - ctInt: cmp := a.IntVals[i] - b.IntVals[i]; + ctInt: cmp := a.IntVals[i].CompareTo(b.IntVals[i]); ctFloat: cmp := a.FloatVals[i].CompareTo(b.FloatVals[i]); ctStr: cmp := a.StrVals[i].CompareTo(b.StrVals[i]); ctBool: cmp := a.BoolVals[i].CompareTo(b.BoolVals[i]); @@ -2982,7 +3014,7 @@ begin )); Result := res; - AssertSchemaConsistent; + Result.AssertSchemaConsistent; end; function DataFrame.Select(colIndices: array of integer): DataFrame; @@ -3090,7 +3122,7 @@ begin Result := res; - AssertSchemaConsistent; + Result.AssertSchemaConsistent; end; function DataFrame.Drop(colIndices: array of integer): DataFrame; @@ -3267,7 +3299,7 @@ begin res.SetSchema(ExtendSchema(name, ctInt, false)); Result := res; - AssertSchemaConsistent; + Result.AssertSchemaConsistent; end; function DataFrame.WithColumnFloat(name: string; f: DataFrameCursor -> real): DataFrame; @@ -3308,7 +3340,7 @@ begin res.SetSchema(ExtendSchema(name, ctFloat, false)); Result := res; - AssertSchemaConsistent; + Result.AssertSchemaConsistent; end; function DataFrame.WithColumnStr(name: string; f: DataFrameCursor -> string): DataFrame; @@ -3349,7 +3381,7 @@ begin res.SetSchema(ExtendSchema(name, ctStr, false)); Result := res; - AssertSchemaConsistent; + Result.AssertSchemaConsistent; end; function DataFrame.WithColumnBool(name: string; f: DataFrameCursor -> boolean): DataFrame; @@ -3390,7 +3422,7 @@ begin res.SetSchema(ExtendSchema(name, ctBool, false)); Result := res; - AssertSchemaConsistent; + Result.AssertSchemaConsistent; end; function DataFrame.ExtendSchema( @@ -3482,9 +3514,21 @@ begin for var i := 0 to columns.Count - 1 do if i <> colIndex then - res.AddColumnView(columns[i]) + res.AddColumnAlias(columns[i]) else res.AddFloatColumn(colName, data, valid); + + // --- ИСПРАВЛЕНИЕ СХЕМЫ --- + var newTypes := Copy(fSchema.Types); + newTypes[colIndex] := ColumnType.ctFloat; + + var newSchema := new DataFrameSchema( + fSchema.ColumnNames, + newTypes, + fSchema.CategoricalFlags + ); + + res.SetSchema(newSchema); Result := res; Result.AssertSchemaConsistent; @@ -3523,10 +3567,22 @@ begin var res := new DataFrame; for var i := 0 to columns.Count - 1 do if i <> colIndex then - res.AddColumnView(columns[i]) + res.AddColumnAlias(columns[i]) else res.AddIntColumn(colName, data, valid); + // --- ИСПРАВЛЕНИЕ СХЕМЫ --- + var newTypes := Copy(fSchema.Types); + newTypes[colIndex] := ColumnType.ctInt; + + var newSchema := new DataFrameSchema( + fSchema.ColumnNames, + newTypes, + fSchema.CategoricalFlags + ); + + res.SetSchema(newSchema); + Result := res; Result.AssertSchemaConsistent; end; @@ -3567,7 +3623,7 @@ begin var res := new DataFrame; for var i := 0 to columns.Count - 1 do - res.AddColumnView(columns[i]); + res.AddColumnAlias(columns[i]); res.AddIntColumn(name, data, valid); @@ -3622,12 +3678,9 @@ begin end; end; - // КЛЮЧЕВОЕ: schema НЕ меняется - res.SetSchema(fSchema); - Result := res; - AssertSchemaConsistent; + Result.AssertSchemaConsistent; end; function DataFrame.TransformFloatColumn(name: string; f: real -> real): DataFrame; @@ -3677,12 +3730,9 @@ begin end; end; - // schema просто копируется - res.SetSchema(fSchema); - Result := res; - AssertSchemaConsistent; + Result.AssertSchemaConsistent; end; function DataFrame.TransformStrColumn(name: string; f: string -> string): DataFrame; @@ -3732,12 +3782,9 @@ begin end; end; - // schema НЕ меняется - res.SetSchema(fSchema); - Result := res; - AssertSchemaConsistent; + Result.AssertSchemaConsistent; end; function DataFrame.TransformBoolColumn(name: string; f: boolean -> boolean): DataFrame; @@ -3787,12 +3834,9 @@ begin end; end; - // schema просто копируется - res.SetSchema(fSchema); - Result := res; - AssertSchemaConsistent; + Result.AssertSchemaConsistent; end; function DataFrame.TakeRows(indices: array of integer): DataFrame; @@ -4256,7 +4300,7 @@ end; /// Добавляет в DataFrame столбец-представление (view), /// использующий те же данные, что и исходный столбец -procedure DataFrame.AddColumnView(src: Column); +procedure DataFrame.AddColumnAlias(src: Column); begin case src.Info.ColType of ctInt: @@ -4309,7 +4353,7 @@ begin res.AddIntColumn(name, data, fc.IsValid); end else - res.AddColumnView(col); + res.AddColumnAlias(col); end; // пересобираем schema (меняются ТИПЫ) @@ -4423,6 +4467,31 @@ begin Result := h; end; +function GroupKey.CompareTo(other: GroupKey): integer; +begin + for var i := 0 to Values.Length - 1 do + begin + var a := Values[i]; + var b := other.Values[i]; + + var cmp := 0; + + if a is integer then + cmp := integer(a).CompareTo(integer(b)) + else if a is string then + cmp := string(a).CompareTo(string(b)) + else if a is boolean then + cmp := boolean(a).CompareTo(boolean(b)) + else + raise new Exception('Unsupported GroupKey type'); + + if cmp <> 0 then + exit(cmp); + end; + + Result := 0; +end; + //----------------------------- // GroupView //----------------------------- @@ -4621,6 +4690,7 @@ begin case df.columns[keyColumn].Info.ColType of ctInt: key := cursor.Int(keyColumn); ctStr: key := cursor.Str(keyColumn); + ctBool: key := cursor.Bool(keyColumn); else Error(ER_GROUPBY_UNSUPPORTED_KEY_TYPE, df.columns[keyColumn].Info.ColType); end; @@ -4658,6 +4728,7 @@ begin case df.columns[c].Info.ColType of ctInt: values[i] := cursor.Int(c); ctStr: values[i] := cursor.Str(c); + ctBool: values[i] := cursor.Bool(c); else Error(ER_GROUPBY_UNSUPPORTED_KEY_TYPE, df.columns[c].Info.ColType); end; end; @@ -4689,29 +4760,51 @@ begin if singleKey then begin - var keys := groups1.Select(kvp -> kvp.Key).ToArray; - var counts := new integer[keys.Length]; - - for var i := 0 to keys.Length - 1 do - counts[i] := groups1[keys[i]].Count; - var col := source.columns[keyColumn]; var keyName := col.Info.Name; - - if col.Info.ColType = ctInt then - begin - res.AddIntColumn(keyName, keys.Select(k -> integer(k)).ToArray, nil); - types.Add(ctInt); - end - else - begin - res.AddStrColumn(keyName, keys.Select(k -> string(k)).ToArray, nil); - types.Add(ctStr); + + var keys: array of object; + + case col.Info.ColType of + ctInt: + keys := groups1.Keys.OrderBy(k -> integer(k)).Select(k -> object(k)).ToArray; + ctStr: + keys := groups1.Keys.OrderBy(k -> string(k)).Select(k -> object(k)).ToArray; + ctBool: + keys := groups1.Keys.OrderBy(k -> boolean(k)).Select(k -> object(k)).ToArray; + else + Error(ER_GROUPBY_UNSUPPORTED_KEY_TYPE); end; - + + var counts := new integer[keys.Length]; + + for var i := 0 to keys.Length - 1 do + counts[i] := groups1[keys[i]].Count; + + // добавление ключевого столбца + case col.Info.ColType of + ctInt: + begin + res.AddIntColumn(keyName, keys.Select(k -> integer(k)).ToArray, nil); + types.Add(ctInt); + end; + + ctStr: + begin + res.AddStrColumn(keyName, keys.Select(k -> string(k)).ToArray, nil); + types.Add(ctStr); + end; + + ctBool: + begin + res.AddBoolColumn(keyName, keys.Select(k -> boolean(k)).ToArray, nil); + types.Add(ctBool); + end; + end; + names.Add(keyName); cats.Add(true); // ключ — categorical - + res.AddIntColumn('count', counts, nil); names.Add('count'); types.Add(ctInt); @@ -4719,7 +4812,7 @@ begin end else begin - var keys := groupsN.Select(kvp -> kvp.Key).ToArray; + var keys := groupsN.Keys.OrderBy(k -> k).ToArray; var counts := new integer[keys.Length]; for var i := 0 to keys.Length - 1 do @@ -4731,15 +4824,27 @@ begin var col := source.columns[ci]; var colName := col.Info.Name; - if col.Info.ColType = ctInt then - begin - res.AddIntColumn(colName, keys.Select(key -> integer(key.Values[k])).ToArray, nil); - types.Add(ctInt); - end - else - begin - res.AddStrColumn(colName, keys.Select(key -> string(key.Values[k])).ToArray, nil); - types.Add(ctStr); + case col.Info.ColType of + ctInt: + begin + res.AddIntColumn(colName, keys.Select(key -> integer(key.Values[k])).ToArray, nil); + types.Add(ctInt); + end; + + ctStr: + begin + res.AddStrColumn(colName, keys.Select(key -> string(key.Values[k])).ToArray, nil); + types.Add(ctStr); + end; + + ctBool: + begin + res.AddBoolColumn(colName, keys.Select(key -> boolean(key.Values[k])).ToArray, nil); + types.Add(ctBool); + end; + + else + Error(ER_GROUPBY_UNSUPPORTED_KEY_TYPE); end; names.Add(colName); @@ -4821,7 +4926,15 @@ begin if singleKey then begin - var keys := groups1.Select(kvp -> kvp.Key).ToArray; + var col := source.columns[keyColumn]; + var keys: array of object; + + case col.Info.ColType of + ctInt: keys := groups1.Keys.OrderBy(k -> integer(k)).Select(k -> object(k)).ToArray; + ctStr: keys := groups1.Keys.OrderBy(k -> string(k)).Select(k -> object(k)).ToArray; + ctBool: keys := groups1.Keys.OrderBy(k -> boolean(k)).Select(k -> object(k)).ToArray; + else Error(ER_GROUPBY_UNSUPPORTED_KEY_TYPE); + end; for var i := 0 to keys.Length - 1 do begin @@ -4834,7 +4947,7 @@ begin end else begin - var keys := groupsN.Select(kvp -> kvp.Key).ToArray; + var keys := groupsN.Keys.OrderBy(k -> k).ToArray; for var i := 0 to keys.Length - 1 do begin @@ -4961,9 +5074,34 @@ begin var keysN: array of GroupKey := nil; if singleKey then - keys1 := groups1.Keys.ToArray + begin + var col := source.columns[keyColumn]; + + case col.Info.ColType of + ctInt: + keys1 := groups1.Keys + .OrderBy(k -> integer(k)) + .Select(k -> object(k)) + .ToArray; + + ctStr: + keys1 := groups1.Keys + .OrderBy(k -> string(k)) + .Select(k -> object(k)) + .ToArray; + + ctBool: + keys1 := groups1.Keys + .OrderBy(k -> boolean(k)) + .Select(k -> object(k)) + .ToArray; + + else + Error(ER_GROUPBY_UNSUPPORTED_KEY_TYPE); + end; + end else - keysN := groupsN.Keys.ToArray; + keysN := groupsN.Keys.OrderBy(k -> k).ToArray; // ---------------------------- // 4. Аллокации только под нужные агрегаты @@ -5199,7 +5337,8 @@ begin types.ToArray, cats.ToArray )); - + + res.AssertSchemaConsistent; Result := res; end; @@ -5261,6 +5400,7 @@ begin end; end; + res.AssertSchemaConsistent; Result := res; end; @@ -5273,29 +5413,47 @@ begin var ix := df.ColumnIndex(colX); var iy := df.ColumnIndex(colY); - var mx := df.Mean(ix); - var my := df.Mean(iy); - var sx := df.Std(ix); - var sy := df.Std(iy); - - if (sx = 0) or (sy = 0) then - Error(ER_ZERO_VARIANCE); - var cur := df.GetCursor; - var sum := 0.0; + var sumX := 0.0; + var sumY := 0.0; var cnt := 0; + // pass 1: mean по пересечению while cur.MoveNext do if cur.IsValid(ix) and cur.IsValid(iy) then begin - sum += (cur.Float(ix) - mx) * (cur.Float(iy) - my); + sumX += cur.Float(ix); + sumY += cur.Float(iy); cnt += 1; end; if cnt = 0 then Error(ER_NO_VALID_PAIRS); - Result := sum / (cnt * sx * sy); + var mx := sumX / cnt; + var my := sumY / cnt; + + // pass 2: covariance и variance + cur := df.GetCursor; + var acc := 0.0; + var accX := 0.0; + var accY := 0.0; + + while cur.MoveNext do + if cur.IsValid(ix) and cur.IsValid(iy) then + begin + var dx := cur.Float(ix) - mx; + var dy := cur.Float(iy) - my; + + acc += dx * dy; + accX += dx * dx; + accY += dy * dy; + end; + + if (accX = 0) or (accY = 0) then + Error(ER_ZERO_VARIANCE); + + Result := acc / Sqrt(accX * accY); end; static function Statistics.CorrelationMatrix(df: DataFrame): DataFrame; @@ -5333,7 +5491,12 @@ begin if i = j then data[i] := 1.0 else - data[i] := Correlation(df, names[i], names[j]); + try + data[i] := Correlation(df, names[i], names[j]); + except + on e: Exception do + data[i] := real.NaN; + end; res.AddFloatColumn(names[j], data, nil); @@ -5355,8 +5518,9 @@ end; static function Statistics.Standardize(df: DataFrame; colName: string): DataFrame; begin var idx := df.ColumnIndex(colName); - var mean := df.Mean(idx); - var std := df.Std(idx); + + var (mean, variance) := df.MeanVariance(idx); + var std := Sqrt(variance); if std = 0 then Error(ER_ZERO_STD_STANDARDIZE); @@ -5369,6 +5533,8 @@ end; static function Statistics.StandardizeAll(df: DataFrame): DataFrame; begin var res := new DataFrame; + var oldSchema := df.Schema; + var cur := df.GetCursor; // 1. заранее считаем mean/std для всех числовых столбцов @@ -5381,8 +5547,9 @@ begin var t := df.GetColumnType(i); if t in [ColumnType.ctInt, ColumnType.ctFloat] then begin - means[i] := df.Mean(i); - stds[i] := df.Std(i); + var (mean, variance) := df.MeanVariance(i); + means[i] := mean; + stds[i] := Sqrt(variance); if stds[i] = 0 then Error(ER_ZERO_STD_COLUMN, df.fSchema.ColumnNames[i]); isNumeric[i] := true; @@ -5395,7 +5562,7 @@ begin if isNumeric[i] then res.AddFloatColumn(df.fSchema.ColumnNames[i], new real[df.RowCount], nil) else - res.AddColumnView(df.columns[i]); // private helper + res.AddColumnAlias(df.GetColumn(i)); end; // 3. заполняем данные @@ -5409,26 +5576,23 @@ begin var col := FloatColumn(res.columns[i]); if cur.IsValid(i) then - begin - col.Data[row] := (cur.Float(i) - means[i]) / stds[i]; - if col.IsValid <> nil then - col.IsValid[row] := true; - end + col.Data[row] := (cur.Float(i) - means[i]) / stds[i] else - begin - // первый NA → создаём IsValid - if col.IsValid = nil then - begin - col.IsValid := new boolean[df.RowCount]; - for var r := 0 to row - 1 do - col.IsValid[r] := true; - end; col.IsValid[row] := false; - end; + end; row += 1; end; + var names := oldSchema.ColumnNames; + var cats := oldSchema.CategoricalFlags; + + var types := new ColumnType[names.Length]; + for var i := 0 to names.Length - 1 do + types[i] := ctFloat; + + res.SetSchema(new DataFrameSchema(names, types, cats)); + Result := res; end; @@ -5443,14 +5607,17 @@ begin Result := df.ReplaceColumnFloat(colName, cur -> begin if not cur.IsValid(idx) then - Error(ER_INVALID_VALUE_IN_COLUMN, colName); - Result := (cur.Float(idx) - mn) / (mx - mn); + Result := real.NaN + else + Result := (cur.Float(idx) - mn) / (mx - mn); end); end; static function Statistics.NormalizeAll(df: DataFrame): DataFrame; begin var res := new DataFrame; + var oldSchema := df.Schema; + var cur := df.GetCursor; // 1. заранее считаем min/max для всех числовых столбцов @@ -5478,7 +5645,7 @@ begin if isNumeric[i] then res.AddFloatColumn(df.fSchema.ColumnNames[i], new real[df.RowCount], nil) else - res.AddColumnView(df.columns[i]); // private helper + res.AddColumnAlias(df.GetColumn(i)); end; // 3. заполняем данные @@ -5511,6 +5678,15 @@ begin end; row += 1; end; + + var names := oldSchema.ColumnNames; + var cats := oldSchema.CategoricalFlags; + + var types := new ColumnType[names.Length]; + for var i := 0 to names.Length - 1 do + types[i] := ctFloat; + + res.SetSchema(new DataFrameSchema(names, types, cats)); Result := res; end; @@ -5940,13 +6116,49 @@ begin var autoCat: array of boolean := nil; var first := true; + + // временные буферы для определения числа колонок + var tmpStarts := new integer[64]; + var tmpLens := new integer[64]; + var actualCount: integer; + var unclosedQuote: boolean; + + ScanFieldsQuoted(linesArray[0], delimiter, tmpStarts, tmpLens, actualCount, unclosedQuote); + + if unclosedQuote then + if strict then + Error(ER_CSV_UNCLOSED_QUOTE); + + // число колонок + var maxColumns := 256; + + var starts := new integer[maxColumns]; + var lens := new integer[maxColumns]; + foreach var line in linesArray index inferRead do begin if inferRead >= inferLimit then break; if first then begin - var parts := line.Split(delimiter); + ScanFieldsQuoted(line, delimiter, starts, lens, actualCount, unclosedQuote); + + if unclosedQuote then + if strict then + Error(ER_CSV_UNCLOSED_QUOTE); + + var parts := new string[actualCount]; + + for var j := 0 to actualCount - 1 do + begin + var s := line.Substring(starts[j] - 1, lens[j]); + + // снять кавычки, если есть + if (s.Length >= 2) and (s[1] = '"') and (s[s.Length] = '"') then + s := s.Substring(1, s.Length - 2); + + parts[j] := s; + end; if hasHeader then begin @@ -6046,7 +6258,24 @@ begin if hasHeader then continue; end; - var parts := line.Split(delimiter); + ScanFieldsQuoted(line, delimiter, starts, lens, actualCount, unclosedQuote); + + if unclosedQuote then + if strict then + Error(ER_CSV_UNCLOSED_QUOTE); + + var parts := new string[actualCount]; + + for var j := 0 to actualCount - 1 do + begin + var s := line.Substring(starts[j]-1, lens[j]); + + // снять кавычки, если есть + if (s.Length >= 2) and (s[1] = '"') and (s[s.Length] = '"') then + s := s.Substring(1, s.Length - 2); + + parts[j] := s; + end; if parts.Length <> originalColCount then if strict then @@ -6164,9 +6393,6 @@ begin strData[j] := new string[rowCount]; end; - var starts := new integer[originalColCount]; - var lens := new integer[originalColCount]; - var row := 0; first := true; foreach var line in linesArray do @@ -6177,9 +6403,6 @@ begin if hasHeader then continue; end; - var actualCount: integer; - var unclosedQuote: boolean; - ScanFieldsQuoted(line, delimiter, starts, lens, actualCount, unclosedQuote); if unclosedQuote then @@ -6423,6 +6646,20 @@ begin ); end; +function EscapeCsv(s: string; delimiter: char): string; +begin + if (s.Contains(delimiter)) or + (s.Contains('"')) or + (s.Contains(#10)) or + (s.Contains(#13)) then + begin + s := s.Replace('"', '""'); + Result := '"' + s + '"'; + end + else + Result := s; +end; + static procedure CsvSaver.Save(df: DataFrame; filename: string; delimiter: char; header: boolean); begin @@ -6438,10 +6675,10 @@ begin // header if header then begin - for var i := 0 to n-1 do + for var i := 0 to n - 1 do begin if i > 0 then w.Write(delimiter); - w.Write(schema.NameAt(i)); + w.Write(EscapeCsv(schema.NameAt(i), delimiter)); end; w.WriteLine; end; @@ -6460,7 +6697,7 @@ begin case schema.ColumnTypeAt(i) of ctInt: w.Write(cur.Int(i)); ctFloat: w.Write(cur.Float(i)); - ctStr: w.Write(cur.Str(i)); + ctStr: w.Write(EscapeCsv(cur.Str(i), delimiter)); ctBool: w.Write(cur.Bool(i)); end; end; diff --git a/bin/Lib/DataFrameABCCore.pas b/bin/Lib/DataFrameABCCore.pas index 0e7e35aee..b58525a17 100644 --- a/bin/Lib/DataFrameABCCore.pas +++ b/bin/Lib/DataFrameABCCore.pas @@ -557,7 +557,6 @@ begin IsValid := valid; end; - constructor IntColumn.Create(name: string); begin inherited Create; @@ -567,34 +566,9 @@ begin IsValid := nil; end; -{procedure IntColumn.AppendInvalid; -begin - Data := Data + [0]; - - if IsValid = nil then - begin - IsValid := new boolean[Length(Data) - 1]; - for var i := 0 to IsValid.Length - 1 do - IsValid[i] := true; - end; - - IsValid := IsValid + [false]; -end;} - -{procedure IntColumn.AppendFromCursor(cur: DataFrameCursor; colIndex: integer); -begin - if cur.IsValid(colIndex) then - begin - Data := Data + [cur.Int(colIndex)]; - if IsValid <> nil then - IsValid := IsValid + [true]; - end - else AppendInvalid; -end;} - function IntColumn.TryGetNumericValue(i: integer; var value: real): boolean; begin - if not IsValid[i] then + if (IsValid <> nil) and not IsValid[i] then exit(false); value := Data[i]; @@ -620,34 +594,9 @@ begin end; -{procedure FloatColumn.AppendFromCursor(cur: DataFrameCursor; colIndex: integer); -begin - if cur.IsValid(colIndex) then - begin - Data := Data + [cur.Float(colIndex)]; - if IsValid <> nil then - IsValid := IsValid + [true]; - end - else AppendInvalid; -end;} - -{procedure FloatColumn.AppendInvalid; -begin - Data := Data + [0.0]; - - if IsValid = nil then - begin - IsValid := new boolean[Length(Data) - 1]; - for var i := 0 to IsValid.Length - 1 do - IsValid[i] := true; - end; - - IsValid := IsValid + [false]; -end;} - function FloatColumn.TryGetNumericValue(i: integer; var value: real): boolean; begin - if not IsValid[i] then + if (IsValid <> nil) and not IsValid[i] then exit(false); value := Data[i]; @@ -673,31 +622,6 @@ begin IsValid := nil; end; -{procedure StrColumn.AppendFromCursor(cur: DataFrameCursor; colIndex: integer); -begin - if cur.IsValid(colIndex) then - begin - Data := Data + [cur.Str(colIndex)]; - if IsValid <> nil then - IsValid := IsValid + [true]; - end - else AppendInvalid; -end;} - -{procedure StrColumn.AppendInvalid; -begin - Data := Data + ['']; - - if IsValid = nil then - begin - IsValid := new boolean[Length(Data) - 1]; - for var i := 0 to IsValid.Length - 1 do - IsValid[i] := true; - end; - - IsValid := IsValid + [false]; -end;} - function StrColumn.TryGetNumericValue(i: integer; var value: real): boolean; begin exit(false); @@ -705,7 +629,7 @@ end; function BoolColumn.TryGetNumericValue(i: integer; var value: real): boolean; begin - if not IsValid[i] then + if (IsValid <> nil) and not IsValid[i] then exit(false); if Data[i] then diff --git a/bin/Lib/LinearAlgebraML.pas b/bin/Lib/LinearAlgebraML.pas index 1561f3f0a..b423c396d 100644 --- a/bin/Lib/LinearAlgebraML.pas +++ b/bin/Lib/LinearAlgebraML.pas @@ -325,6 +325,9 @@ const 'Для QR-разложения требуется m >= n!!QR decomposition requires m >= n'; ER_SINGULAR_MATRIX = 'Матрица вырождена или плохо обусловлена!!Matrix is singular or ill-conditioned'; + ER_EMPTY_MATRIX = + 'Матрица пуста!!Matrix is empty'; + type MLNotSPDException = class(MLException); @@ -470,7 +473,7 @@ end; function Vector.Average: real; begin if Length = 0 then - ArgumentError('Vector is empty'); + ArgumentError(ER_VECTOR_EMPTY); Result := Sum / Length; end; @@ -646,6 +649,10 @@ function Matrix.ColumnMins: Vector; begin var n := RowCount; var p := ColCount; + + if n = 0 then + exit(new Vector(p)); + Result := new Vector(p); for var j := 0 to p - 1 do @@ -662,6 +669,10 @@ function Matrix.ColumnMaxs: Vector; begin var n := RowCount; var p := ColCount; + + if n = 0 then + exit(new Vector(p)); + Result := new Vector(p); for var j := 0 to p - 1 do @@ -678,6 +689,10 @@ function Matrix.RowMins: Vector; begin var n := RowCount; var p := ColCount; + + if n = 0 then + exit(new Vector(p)); + Result := new Vector(n); for var i := 0 to n - 1 do @@ -694,6 +709,10 @@ function Matrix.RowMaxs: Vector; begin var n := RowCount; var p := ColCount; + + if n = 0 then + exit(new Vector(p)); + Result := new Vector(n); for var i := 0 to n - 1 do @@ -782,10 +801,15 @@ end; function Matrix.ColumnArgMin(j: integer): integer; begin + var n := RowCount; + + if n = 0 then + Error(ER_EMPTY_MATRIX); + var minVal := fdata[0,j]; var arg := 0; - for var i := 1 to RowCount - 1 do + for var i := 1 to n - 1 do if fdata[i,j] < minVal then begin minVal := fdata[i,j]; @@ -802,10 +826,15 @@ end; function Matrix.ColumnArgMax(j: integer): integer; begin + var n := RowCount; + + if n = 0 then + Error(ER_EMPTY_MATRIX); + var maxVal := fdata[0,j]; var arg := 0; - for var i := 1 to RowCount - 1 do + for var i := 1 to n - 1 do if fdata[i,j] > maxVal then begin maxVal := fdata[i,j]; @@ -1388,7 +1417,7 @@ begin maxRow := i; end; - if maxVal = 0.0 then + if maxVal < 1e-12 then Error(ER_MATRIX_SINGULAR); if maxRow <> k then @@ -1604,6 +1633,9 @@ begin for var i := k to m - 1 do vnorm2 += R[i,k] * R[i,k]; + if vnorm2 < 1e-12 then + continue; + var beta := 2.0 / vnorm2; // ---- применить отражение к R diff --git a/bin/Lib/MLDatasets.pas b/bin/Lib/MLDatasets.pas index 9a98e35ce..25337e6c1 100644 --- a/bin/Lib/MLDatasets.pas +++ b/bin/Lib/MLDatasets.pas @@ -259,7 +259,7 @@ type /// Датасет российских городов (задача кластеризации) static function RussianCities: Dataset; - /// Датасет результатов экзамена студентов (классификация) + {/// Датасет результатов экзамена студентов (классификация) static function StudentExam: Dataset; /// Датасет банковских клиентов (классификация одобрения кредита) @@ -272,7 +272,7 @@ type static function MoscowTransport: Dataset; /// Датасет интернет-покупок пользователей (классификация покупки) - static function OnlineShopping: Dataset; + static function OnlineShopping: Dataset;} static function LoadMeta(path: string): Dictionary; static function ParseFeatures(meta: Dictionary): array of string; @@ -330,6 +330,11 @@ const 'Неподдерживаемый тип ключа для группировки!!Unsupported key type for grouping'; ER_STRATIFIED_ONLY_FOR_CLASSIFICATION = 'Стратифицированное разбиение доступно только для задач классификации!!Stratified split is only for classification tasks'; + ER_CLASS_BALANCE_TOO_SMALL = + 'Слишком малое значение classBalance: {0}. Минимально допустимое значение — 1e-3!!classBalance is too small: {0}. Minimum allowed value is 1e-3'; + ER_UNSUPPORTED_TARGET_TYPE = + 'Неподдерживаемый тип целевого столбца: {0}!!Unsupported target column type: {0}'; + C_DATASET = 'Датасет: {0}!!Dataset: {0}'; C_DESCRIPTION = 'Описание:!!Description:'; @@ -593,7 +598,14 @@ begin if Task <> TaskType.Classification then ArgumentError(ER_CLASSES_ONLY_CLASSIFICATION); - Result := Data.GetStrColumn(Target).Distinct.ToArray; + var idx := Data.ColumnIndex(Target); + var t := Data.GetColumnType(idx); + + case t of + ctStr: Result := Data.GetStrColumn(Target).Distinct.ToArray; + ctInt: Result := Data.GetIntColumn(Target).Distinct.Select(x -> x.ToString).ToArray; + else Error(ER_UNSUPPORTED_TARGET_TYPE, t); + end; end; function Dataset.ClassCounts: Dictionary; @@ -678,6 +690,9 @@ begin if (classBalance <= 0) or (classBalance > 1) then ArgumentOutOfRangeError(ER_PARAM_RANGE_01, 'classBalance'); + if classBalance < 1e-3 then + ArgumentOutOfRangeError(ER_CLASS_BALANCE_TOO_SMALL, classBalance); + if noisePoints < 0 then ArgumentOutOfRangeError(ER_PARAM_GE_ZERO, 'noisePoints'); @@ -1301,7 +1316,7 @@ begin Result := ds; end; -static function Datasets.StudentExam: Dataset; +{static function Datasets.StudentExam: Dataset; begin NotImplementedError(ER_NOT_IMPLEMENTED, 'Datasets.StudentExam'); Result := nil; @@ -1329,7 +1344,7 @@ static function Datasets.OnlineShopping: Dataset; begin NotImplementedError(ER_NOT_IMPLEMENTED, 'Datasets.OnlineShopping'); Result := nil; -end; +end;} static function Datasets.LoadMeta(path: string): Dictionary; begin diff --git a/bin/Lib/MLModelsABC.pas b/bin/Lib/MLModelsABC.pas index 23d204827..4fdd26ceb 100644 --- a/bin/Lib/MLModelsABC.pas +++ b/bin/Lib/MLModelsABC.pas @@ -486,11 +486,13 @@ type // DecisionTreeBase //============================ /// Базовый абстрактный класс дерева решений. +/// Используется только DecisionTreeRegressor +/// Classifier использует DecisionTreeCore /// Реализует общую логику построения структуры дерева: /// рекурсивное разбиение, контроль глубины, /// минимального числа объектов и расчет важности признаков. -/// Конкретная логика вычисления значения листа и критерия разбиения задается в наследниках - DecisionTreeBase = abstract class(ITreeModel) +/// Конкретная логика вычисления значения листа и критерия разбиения задается в наследнике + DecisionTreeRegressorBase = abstract class(ITreeModel) protected fRoot: DecisionTreeNode; fMaxDepth: integer; @@ -516,7 +518,7 @@ type function LeafValue(y: Vector; indices: array of integer): real; virtual; abstract; function LeafNode(value: real): DecisionTreeNode; - procedure CopyBaseState(dest: DecisionTreeBase); + procedure CopyBaseState(dest: DecisionTreeRegressorBase); function GetFeatureSubset(nFeatures: integer): array of integer; virtual; procedure SetRowIndices(rows: array of integer); @@ -563,20 +565,23 @@ type /// Дерево решений для задачи классификации. /// Использует критерий нечистоты (обычно Gini) для выбора оптимальных разбиений. /// В листьях хранится наиболее частый класс - DecisionTreeClassifier = class(DecisionTreeBase, IClassifier) + DecisionTreeClassifier = class(IClassifier) private + fMaxDepth: integer; + fMinSamplesSplit: integer; + fMinSamplesLeaf: integer; + fFitted: boolean; + fCriterion: ISplitCriterion; + fFeatureImportances: Vector; + fRandomSeed: integer; + fMaxFeatures: integer := 0; + fUserProvidedSeed: boolean; + fRng: System.Random; + fCore: DecisionTreeCore; fIndexToClass: array of integer; fClassLabels: array of string; - function PredictOne(X: Matrix; rowIndex: integer): integer; - function MajorityClass(y: Vector; indices: array of integer): integer; - - protected - function LeafValue(y: Vector; indices: array of integer): real; override; - - function FindBestSplit(X: Matrix; y: Vector; indices: array of integer): SplitResult; override; - public /// Создает классификационное дерево: /// • maxDepth — максимальная глубина дерева (-1 означает без ограничения). @@ -589,11 +594,11 @@ type /// X — матрица признаков. /// y — вектор целевых меток (целые значения). /// Строит структуру дерева путем минимизации нечистоты в узлах. - function Fit(X: Matrix; y: Vector): ISupervisedModel; override; + function Fit(X: Matrix; y: Vector): ISupervisedModel; /// Выполняет предсказание меток классов для X. /// Для каждого объекта возвращается класс, соответствующий листу дерева. - function Predict(X: Matrix): Vector; override; + function Predict(X: Matrix): Vector; /// Возвращает предсказанные метки классов для объектов из X. /// Каждый элемент результата — индекс класса (целое число). @@ -603,7 +608,7 @@ type /// Создает глубокую копию дерева классификации. /// Копируется структура узлов, параметры и обученное состояние. - function Clone: IModel; override; + function Clone: IModel; /// Возвращает строковое представление модели. function ToString: string; override; @@ -617,6 +622,10 @@ type procedure SetClassLabels(classes: array of string); function GetClassLabels: array of string; + +/// Возвращает true, если дерево обучено. +/// Если false — Predict вызовет ошибку. + property IsFitted: boolean read fFitted; end; //============================ @@ -627,7 +636,7 @@ type /// Использует критерий дисперсии для выбора разбиений. /// В листьях хранится среднее значение целевой переменной. /// Поддерживает L2-регуляризацию значения листа (leafL2) - DecisionTreeRegressor = class(DecisionTreeBase, IRegressor) + DecisionTreeRegressor = class(DecisionTreeRegressorBase, IRegressor) private fLeafL2: real; @@ -2633,7 +2642,7 @@ end; constructor LassoRegression.Create(alpha: real; maxIter: integer; tol: real); begin // Lasso = ElasticNet с L2 = 0 - new ElasticNet(alpha, 0.0, maxIter, tol); + fModel := new ElasticNet(alpha, 0.0, maxIter, tol); end; function LassoRegression.Fit(X: Matrix; y: Vector): ISupervisedModel; @@ -2810,7 +2819,7 @@ begin // --- convergence check if fCheckConvergence then begin - if Abs(prevLoss - loss) < fTol then + if Abs(prevLoss - loss) < Max(fTol, fMinImprovement) then break; prevLoss := loss; @@ -2934,7 +2943,7 @@ begin Result := new integer[v.Length]; for var i := 0 to v.Length - 1 do - Result[i] := integer(v[i]); + Result[i] := Round(v[i]); end; function LogisticRegression.ToString: string; @@ -3490,7 +3499,7 @@ end; // DecisionTreeBase -constructor DecisionTreeBase.Create( +constructor DecisionTreeRegressorBase.Create( maxDepth: integer; minSamplesSplit: integer; minSamplesLeaf: integer; @@ -3536,7 +3545,7 @@ begin fRng := new System.Random(fRandomSeed); end; -procedure DecisionTreeBase.CopyBaseState(dest: DecisionTreeBase); +procedure DecisionTreeRegressorBase.CopyBaseState(dest: DecisionTreeRegressorBase); begin dest.fMaxDepth := fMaxDepth; dest.fMinSamplesSplit := fMinSamplesSplit; @@ -3566,7 +3575,7 @@ begin dest.fRowIndices := Copy(fRowIndices); end; -function DecisionTreeBase.GetFeatureSubset(nFeatures: integer): array of integer; +function DecisionTreeRegressorBase.GetFeatureSubset(nFeatures: integer): array of integer; begin if (fMaxFeatures = 0) or (fMaxFeatures >= nFeatures) then begin @@ -3592,7 +3601,7 @@ begin Result := subset; end; -procedure DecisionTreeBase.SetRowIndices(rows: array of integer); +procedure DecisionTreeRegressorBase.SetRowIndices(rows: array of integer); begin if Length(rows) = 0 then ArgumentError('Row subset cannot be empty!!Row subset cannot be empty'); @@ -3600,7 +3609,7 @@ begin fRowIndices := Copy(rows); end; -function DecisionTreeBase.FeatureImportances: Vector; +function DecisionTreeRegressorBase.FeatureImportances: Vector; begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); @@ -3611,7 +3620,7 @@ begin Result := fFeatureImportances.Clone; end; -function DecisionTreeBase.LeafNode(value: real): DecisionTreeNode; +function DecisionTreeRegressorBase.LeafNode(value: real): DecisionTreeNode; begin var n := new DecisionTreeNode; n.IsLeaf := true; @@ -3619,7 +3628,7 @@ begin Result := n; end; -function DecisionTreeBase.BuildTree(X: Matrix; y: Vector; +function DecisionTreeRegressorBase.BuildTree(X: Matrix; y: Vector; indices: array of integer; depth: integer): DecisionTreeNode; begin if (fMaxDepth >= 0) and (depth >= fMaxDepth) then @@ -3695,7 +3704,7 @@ end; const EPS = 1e-12; -function DecisionTreeBase.IsPure(y: Vector; indices: array of integer): boolean; +function DecisionTreeRegressorBase.IsPure(y: Vector; indices: array of integer): boolean; begin Result := fCriterion.Impurity(y, indices) < EPS; end; @@ -3823,180 +3832,37 @@ end; // DecisionTreeClassifier //============================== -function DecisionTreeClassifier.FindBestSplit(X: Matrix; y: Vector; indices: array of integer): SplitResult; +constructor DecisionTreeClassifier.Create(maxDepth: integer; minSamplesSplit: integer; minSamplesLeaf: integer; criterion: ISplitCriterion; seed: integer); begin - var n := indices.Length; - if n <= 1 then - exit(SplitResult.Invalid); + if maxDepth = 0 then + ArgumentOutOfRangeError(ER_MAX_DEPTH_INVALID, maxDepth); - var p := X.ColCount; + if minSamplesSplit < 2 then + ArgumentOutOfRangeError(ER_MIN_SAMPLES_SPLIT_INVALID, minSamplesSplit); - var bestFeature := -1; - var bestThreshold := 0.0; - var bestScore := real.PositiveInfinity; + if minSamplesLeaf < 1 then + ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf); - var feat := new integer[p]; - for var i := 0 to p - 1 do - feat[i] := i; + if minSamplesLeaf >= minSamplesSplit then + ArgumentOutOfRangeError(ER_MIN_LEAF_GE_SPLIT, minSamplesLeaf, minSamplesSplit); - var m := fMaxFeatures; - if (m <= 0) or (m > p) then - m := p; + fMaxDepth := maxDepth; + fMinSamplesSplit := minSamplesSplit; + fMinSamplesLeaf := minSamplesLeaf; + fCriterion := if criterion = nil then new GiniCriterion else criterion; - // partial Fisher–Yates - for var i := 0 to m - 1 do + if seed < 0 then begin - var j := fRng.Next(i, p); - var tmp := feat[i]; - feat[i] := feat[j]; - feat[j] := tmp; + fUserProvidedSeed := false; + fRandomSeed := System.Environment.TickCount and integer.MaxValue; + end + else + begin + fUserProvidedSeed := true; + fRandomSeed := seed; end; - for var fi := 0 to m - 1 do - begin - var feature := feat[fi]; - - var values := new real[n]; - var labels := new integer[n]; - - for var i := 0 to n - 1 do - begin - var row := indices[i]; - values[i] := X[row, feature]; - - var cls := Round(y[row]); - - if (cls < 0) or (cls >= ClassCount) then - ArgumentError(ER_LABEL_INDEX_INVALID); - - labels[i] := cls; - end; - - System.Array.Sort(values, labels); - - var rightCounts := new integer[ClassCount]; - for var i := 0 to n - 1 do - rightCounts[labels[i]] += 1; - - var leftCounts := new integer[ClassCount]; - - var leftSize := 0; - var rightSize := n; - - for var i := 0 to n - 2 do - begin - var cls := labels[i]; - - leftCounts[cls] += 1; - rightCounts[cls] -= 1; - - leftSize += 1; - rightSize -= 1; - - if values[i] = values[i + 1] then - continue; - - if (leftSize < fMinSamplesLeaf) or (rightSize < fMinSamplesLeaf) then - continue; - - // ----- GINI LEFT ----- - var giniLeft := 1.0; - - for var c := 0 to ClassCount - 1 do - begin - if leftCounts[c] > 0 then - begin - var q := leftCounts[c] / real(leftSize); - giniLeft -= q * q; - end; - end; - - if giniLeft < 0 then - giniLeft := 0.0; - - // ----- GINI RIGHT ----- - var giniRight := 1.0; - - for var c := 0 to ClassCount - 1 do - begin - if rightCounts[c] > 0 then - begin - var q := rightCounts[c] / real(rightSize); - giniRight -= q * q; - end; - end; - - if giniRight < 0 then - giniRight := 0.0; - - var weighted := - (real(leftSize) / n) * giniLeft + - (real(rightSize) / n) * giniRight; - - if double.IsNaN(weighted) then - continue; - - if weighted < bestScore then - begin - bestScore := weighted; - bestFeature := feature; - bestThreshold := (values[i] + values[i + 1]) * 0.5; - end; - end; - end; - - if bestFeature = -1 then - exit(SplitResult.Invalid); - - Result := SplitResult.Create(bestFeature, bestThreshold); -end; - -function DecisionTreeClassifier.PredictOne(X: Matrix; rowIndex: integer): integer; -begin - var node := fRoot; - - while not node.IsLeaf do - begin - if X[rowIndex, node.FeatureIndex] <= node.Threshold then - node := node.Left - else - node := node.Right; - end; - - Result := integer(node.LeafValue); // внутренний индекс -end; - -function DecisionTreeClassifier.MajorityClass(y: Vector; indices: array of integer): integer; -begin - var counts := new integer[ClassCount]; - - // Подсчёт частот - foreach var i in indices do - begin - var c := Round(y[i]); - counts[c] += 1; - end; - - // Поиск максимума - var bestClass := 0; - var bestCount := -1; - - for var c := 0 to ClassCount - 1 do - if counts[c] > bestCount then - begin - bestCount := counts[c]; - bestClass := c; - end; - - Result := bestClass; -end; - -constructor DecisionTreeClassifier.Create(maxDepth: integer; minSamplesSplit: integer; minSamplesLeaf: integer; - criterion: ISplitCriterion; seed: integer); -begin - inherited Create(maxDepth, minSamplesSplit, minSamplesLeaf, - (if criterion = nil then new GiniCriterion else criterion), - seed); + fRng := new System.Random(fRandomSeed); end; function DecisionTreeClassifier.Fit(X: Matrix; y: Vector): ISupervisedModel; @@ -4060,7 +3926,8 @@ begin fMinSamplesSplit, fMinSamplesLeaf, fCriterion, - 0 // maxFeatures = 0 -> использовать все признаки + fMaxFeatures, + fRandomSeed ); fCore.Fit(X, yEncoded); @@ -4115,7 +3982,24 @@ begin fRandomSeed ); - CopyBaseState(m); + m.fMaxDepth := fMaxDepth; + m.fMinSamplesSplit := fMinSamplesSplit; + m.fMinSamplesLeaf := fMinSamplesLeaf; + m.fFitted := fFitted; + m.fRandomSeed := fRandomSeed; + m.fMaxFeatures := fMaxFeatures; + m.fUserProvidedSeed := fUserProvidedSeed; + + if fUserProvidedSeed then + m.fRng := new System.Random(fRandomSeed) + else + m.fRng := new System.Random; + + if fCriterion <> nil then + m.fCriterion := fCriterion; + + if fFeatureImportances <> nil then + m.fFeatureImportances := fFeatureImportances.Clone; // --- classes (единственный источник истины) if fIndexToClass <> nil then @@ -4134,20 +4018,16 @@ begin fMinSamplesSplit, fMinSamplesLeaf, fCriterion, - fMaxFeatures + fMaxFeatures, + fRandomSeed ); - m.fCore.fRoot := fCore.fRoot.Clone; // ключевой момент + m.fCore.fRoot := fCore.fRoot.Clone; end; Result := m; end; -function DecisionTreeClassifier.LeafValue(y: Vector; indices: array of integer): real; -begin - Result := MajorityClass(y, indices); -end; - function DecisionTreeClassifier.ToString: string; begin Result := @@ -4907,7 +4787,7 @@ begin Result := new integer[v.Length]; for var i := 0 to v.Length - 1 do - Result[i] := integer(v[i]); + Result[i] := Round(v[i]); end; function RandomForestClassifier.PredictProba(X: Matrix): Matrix; @@ -5517,19 +5397,8 @@ begin var deltaTrain := tree.Predict(XTrain); // --- update TRAIN --- - if useSubsample then - begin - for var i := 0 to rows.Length - 1 do - begin - var idx := rows[i]; - yPredTrain[idx] += fLearningRate * deltaTrain[idx]; - end; - end - else - begin - for var i := 0 to nTrain - 1 do - yPredTrain[i] += fLearningRate * deltaTrain[i]; - end; + for var i := 0 to nTrain - 1 do + yPredTrain[i] += fLearningRate * deltaTrain[i]; // --- update OOB --- if useOOB then @@ -6128,10 +5997,7 @@ begin begin for var i := 0 to nTrain - 1 do if not inBag[i] then - begin logitsOOB[i, cls] += fLearningRate * deltaTrain[i]; - oobCount[i] += 1; - end; end; if useValidation then @@ -6141,6 +6007,13 @@ begin logitsVal[i, cls] += fLearningRate * deltaVal[i]; end; end; + + if useOOB then + begin + for var i := 0 to nTrain - 1 do + if not inBag[i] then + oobCount[i] += 1; + end; fEstimators.Add(trees); @@ -6801,7 +6674,7 @@ begin Result := new integer[v.Length]; for var i := 0 to v.Length - 1 do - Result[i] := integer(v[i]); + Result[i] := Round(v[i]); end; procedure GradientBoostingClassifier.SetClassLabels(classes: array of string); @@ -7146,7 +7019,7 @@ begin Result := new integer[v.Length]; for var i := 0 to v.Length - 1 do - Result[i] := integer(v[i]); + Result[i] := Round(v[i]); end; function KNNClassifier.PredictProba(X: Matrix): Matrix; diff --git a/bin/Lib/MLPipelineABC.pas b/bin/Lib/MLPipelineABC.pas index f0a0e60b5..9669e1366 100644 --- a/bin/Lib/MLPipelineABC.pas +++ b/bin/Lib/MLPipelineABC.pas @@ -50,7 +50,7 @@ type /// • target — целевая переменная. /// /// Если модель не добавлена, DataPipeline работает как чистый DF-конвейер (Fit/Transform/FitTransform) - DataPipeline = class(PipelineBase) + DataPipeline = class(PipelineBase, IModel) private fModel: ISupervisedModel; fTask: TaskKind; @@ -112,6 +112,10 @@ type function GetClassLabels: array of string; function ToString: string; override; + + function Name: string := Self.GetType.Name; + + function Clone: IModel; end; /// UDataPipeline — конвейер подготовки данных и обучения модели без учителя на DataFrame. @@ -131,7 +135,7 @@ type /// /// Если модель не добавлена, UDataPipeline работает как чистый DF-конвейер /// (Fit/Transform/FitTransform) - UDataPipeline = class(PipelineBase) + UDataPipeline = class(PipelineBase, IModel) private //fDataSteps: List; //fMatrixSteps: List; @@ -198,6 +202,10 @@ type function PredictLabels(df: DataFrame): array of integer; function ToString: string; override; + + function Clone: IModel; + + function Name: string := Self.GetType.Name; end; implementation @@ -304,6 +312,8 @@ const ER_MODEL_NOT_CLASSIFIER = 'Модель не является классификатором!!' + 'Model is not a classifier'; + ER_MODEL_CLONE_TYPE = + 'Clone модели вернул неподдерживаемый тип!!Model Clone returned unsupported type'; //----------------------------- // DataPipeline @@ -588,6 +598,10 @@ begin if feats.Count = 0 then ArgumentError(ER_PIPELINE_NO_FEATURES); + // fFinalFeatures — признаки DataFrame (ДО matrix-transform) + // После FitTransformMatrix пространство признаков меняется (PCA и т.п.), + // но fFinalFeatures используется только для ToMatrix в Predict. + fFinalFeatures := feats.ToArray; var X := current.ToMatrix(fFinalFeatures); @@ -611,10 +625,12 @@ begin // --- 3) Matrix transformers X := FitTransformMatrix(X, y); - + // После этого X уже не соответствует fFinalFeatures по размерности, + // но это нормально: fFinalFeatures используется только до ToMatrix. + // --- 4) модель fModel := fModel.Fit(X, y); - + if fTask = tkClassification then begin if fModel is IClassifier(var cls) then @@ -671,18 +687,10 @@ begin ArgumentError(ER_MODEL_NULL); var current := Transform(df); - - if fFinalFeatures = nil then - Error(ER_PIPELINE_FINALFEATURES); - - for var i := 0 to High(fFinalFeatures) do - if not current.HasColumn(fFinalFeatures[i]) then - ArgumentError(ER_PIPELINE_FEATURE_NOT_FOUND, fFinalFeatures[i]); var X := current.ToMatrix(fFinalFeatures); - X := TransformMatrix(X); - + if not (fModel is IPredictiveModel) then Error(ER_PREDICT_NOT_SUPPORTED); @@ -703,16 +711,8 @@ begin ArgumentError(ER_PROBA_NOT_SUPPORTED); var current := Transform(df); - - if fFinalFeatures = nil then - Error(ER_PIPELINE_FINALFEATURES); - - for var i := 0 to High(fFinalFeatures) do - if not current.HasColumn(fFinalFeatures[i]) then - ArgumentError(ER_PIPELINE_FEATURE_NOT_FOUND, fFinalFeatures[i]); - - var X := current.ToMatrix(fFinalFeatures); + var X := current.ToMatrix(fFinalFeatures); X := TransformMatrix(X); Result := (fModel as IProbabilisticClassifier).PredictProba(X); @@ -807,6 +807,36 @@ begin Result := sb; end; +function DataPipeline.Clone: IModel; +begin + var p := new DataPipeline; + + // --- конфигурация + p.fFeatures := Copy(fFeatures); + p.fTarget := fTarget; + p.fTask := fTask; + + // --- шаги (глубокая копия) + p.fDataSteps := fDataSteps.Select(s -> s.Clone).ToList; + p.fMatrixSteps := fMatrixSteps.Select(s -> s.Clone).ToList; + + // --- модель (глубокая копия конфигурации) + if fModel <> nil then + begin + var m := fModel.Clone; + if not (m is ISupervisedModel) then + Error(ER_MODEL_CLONE_TYPE); + + p.fModel := m as ISupervisedModel; + end; + + // --- состояние НЕ копируем + p.fFinalFeatures := nil; + p.fFitted := false; + + Result := p; +end; + //----------------------------- // UDataPipeline //----------------------------- @@ -1195,4 +1225,26 @@ begin Result := sb; end; +function UDataPipeline.Clone: IModel; +begin + var p := new UDataPipeline; + + // --- конфигурация + p.fFeatures := Copy(fFeatures); + + // --- шаги (глубокая копия) + p.fDataSteps := fDataSteps.Select(s -> s.Clone).ToList; + p.fMatrixSteps := fMatrixSteps.Select(s -> s.Clone).ToList; + + // --- модель + if fModel <> nil then + p.fModel := fModel.Clone as IUnsupervisedModel; + + // --- состояние НЕ копируем + p.fFinalFeatures := nil; + p.fFitted := false; + + Result := p; +end; + end. \ No newline at end of file diff --git a/bin/Lib/MetricsABC.pas b/bin/Lib/MetricsABC.pas index 56714d42f..ac46a433f 100644 --- a/bin/Lib/MetricsABC.pas +++ b/bin/Lib/MetricsABC.pas @@ -994,7 +994,7 @@ begin var correct := 0; for var i := 0 to n - 1 do - if yTrue[i] = yPred[i] then + if Round(yTrue[i]) = Round(yPred[i]) then correct += 1; Result := correct / n; @@ -1667,6 +1667,9 @@ end; /// 1 — идеальное совпадение кластеризации и истинных меток /// 0 — случайное разбиение /// <0 — хуже случайного +/// Сложность: O(n²). При больших n может быть медленным. +/// Для больших выборок рекомендуется использовать альтернативную реализацию +/// через таблицу сопряжённости (contingency table) с асимптотикой O(n · k) static function Metrics.AdjustedRandIndex(yTrue, yPred: Vector): real; begin if yTrue.Length <> yPred.Length then diff --git a/bin/Lib/PlotML.pas b/bin/Lib/PlotML.pas index a545f3e26..967ed2fa2 100644 --- a/bin/Lib/PlotML.pas +++ b/bin/Lib/PlotML.pas @@ -785,6 +785,7 @@ begin var container := rootChart.Content as GridWPF; if container <> nil then container.Children.Clear; + rootPaletteIndex := 0; end); end; diff --git a/bin/Lib/PreprocessorABC.pas b/bin/Lib/PreprocessorABC.pas index 8f646774a..469df3f99 100644 --- a/bin/Lib/PreprocessorABC.pas +++ b/bin/Lib/PreprocessorABC.pas @@ -35,6 +35,8 @@ type function Transform(df: DataFrame): DataFrame; /// Выполняет Fit и Transform последовательно function FitTransform(df: DataFrame): DataFrame; + + function Clone: IPreprocessor; end; /// Кодирует строковый категориальный столбец в целочисленные индексы (0,1,2,...). @@ -62,6 +64,8 @@ type function ToString: string; override; property ColumnName: string read col; + + function Clone: IPreprocessor; end; /// Кодирует строковый категориальный столбец в набор бинарных (one-hot) столбцов @@ -89,6 +93,8 @@ type function ToString: string; override; property ColumnName: string read col; + + function Clone: IPreprocessor; end; ImputeStrategy = (isMean, isConstant, isMedian); @@ -123,6 +129,8 @@ type function ToString: string; override; property Columns: array of string read cols; + + function Clone: IPreprocessor; end; @@ -194,6 +202,9 @@ const 'Массив констант не задан или имеет неверный размер!!Constants array is null or has invalid length'; ER_IMPUTER_STRATEGY_NOT_SUPPORTED = 'Стратегия импутации {0} не поддерживается!!Imputation strategy {0} is not supported'; + ER_UNSUPPORTED_IMPUTE_STRATEGY = + 'Неподдерживаемая стратегия заполнения: {0}!!Unsupported impute strategy: {0}'; + //----------------------------- // LabelEncoder @@ -287,7 +298,7 @@ begin foreach var src in df.GetColumns do if src.Info.Name <> col then - res.AddColumnView(src) + res.AddColumnAlias(src) else res.AddIntColumn(col, data, valid); @@ -305,6 +316,10 @@ begin Result := 'LabelEncoder(' + col + ')'; end; +function LabelEncoder.Clone: IPreprocessor; +begin + Result := new LabelEncoder(col); +end; //----------------------------- // OneHotEncoder //----------------------------- @@ -414,6 +429,11 @@ begin Result := 'OneHotEncoder(column=' + col + ')'; end; +function OneHotEncoder.Clone: IPreprocessor; +begin + Result := new OneHotEncoder(col); +end; + //----------------------------- // Imputer //----------------------------- @@ -546,8 +566,10 @@ begin for var i := 0 to cols.Length - 1 do begin var name := cols[i]; - var idx := df.Schema.IndexOf(name); - var ct := df.Schema.ColumnTypeAt(idx); + + // --- ВАЖНО: используем актуальную схему + var idx := res.Schema.IndexOf(name); + var ct := res.Schema.ColumnTypeAt(idx); if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then Error(ER_IMPUTER_COLUMN_NOT_NUMERIC, name); @@ -571,12 +593,19 @@ begin if ct = ColumnType.ctInt then begin var k: integer; - try - k := integer(v); - except - on e: Exception do + + if v is integer then + k := integer(v) + else if v is real then + begin + var r := real(v); + var ir := Round(r); + if Abs(r - ir) > 1e-9 then Error(ER_IMPUTER_CONSTANT_TYPE_MISMATCH, name); - end; + k := ir; + end + else + Error(ER_IMPUTER_CONSTANT_TYPE_MISMATCH, name); res := res.ReplaceColumnInt( name, @@ -599,7 +628,7 @@ begin ); end; end; - + isMedian: begin var m := medians[i]; @@ -607,7 +636,7 @@ begin name, c -> (if c.IsValid(idx) then c.Float(idx) else m) ); - end; + end; else Error(ER_IMPUTER_STRATEGY_NOT_SUPPORTED, strategy); @@ -651,4 +680,24 @@ begin end; end; +function Imputer.Clone: IPreprocessor; +begin + case strategy of + ImputeStrategy.isMean: + Result := new Imputer(cols); + + ImputeStrategy.isMedian: + Result := new Imputer(ImputeStrategy.isMedian, cols); + + ImputeStrategy.isConstant: + begin + var val := if (constants <> nil) and (constants.Length > 0) then constants[0] else nil; + Result := new Imputer(val, cols); + end; + + else + Error(ER_UNSUPPORTED_IMPUTE_STRATEGY, strategy); + end; +end; + end. \ No newline at end of file diff --git a/bin/Lib/ValidationML.pas b/bin/Lib/ValidationML.pas index 592c93069..63b568954 100644 --- a/bin/Lib/ValidationML.pas +++ b/bin/Lib/ValidationML.pas @@ -108,7 +108,9 @@ const 'Некорректное значение параметра {0}!!Invalid value for parameter {0}'; ER_DATASET_TOO_SMALL = 'Для {0} требуется как минимум 2 объекта!!' + - 'At least 2 samples are required for {0}'; + 'At least 2 samples are required for {0}'; + ER_STRATIFIED_CLASS_TOO_SMALL = + 'Класс {0} содержит {1} объектов, что меньше числа фолдов ({2})!!Class {0} has {1} samples, which is less than the number of folds ({2})'; //----------------------------- // Validation @@ -319,7 +321,7 @@ begin if cnt < k then ArgumentError( - 'StratifiedKFold: класс %d содержит %d объектов, что меньше числа фолдов (%d)', + ER_STRATIFIED_CLASS_TOO_SMALL, cls, cnt, k ); end;