ML - исправление ошибок и неточностей (внешний аудит)

This commit is contained in:
Mikhalkovich Stanislav 2026-04-13 21:05:51 +03:00
parent b9aa6ae65d
commit 9cf33b094c
10 changed files with 653 additions and 465 deletions

View file

@ -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<string, DescribeStats>;
/// Группирует данные по столбцу по индексу
/// Группирует данные по столбцу по индексу.
/// Поддерживаемые типы ключей: 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<GroupKey>)
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<real, List<integer>>;
Error(ER_JOIN_FLOAT_KEY_NOT_SUPPORTED);
Result := nil;
{var index := new Dictionary<real, List<integer>>;
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<real, List<integer>>;
Error(ER_JOIN_FLOAT_KEY_NOT_SUPPORTED);
Result := nil;
{var index := new Dictionary<real, List<integer>>;
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,10 +3514,22 @@ 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;
end;
@ -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,26 +4760,48 @@ begin
if singleKey then
begin
var keys := groups1.Select(kvp -> kvp.Key).ToArray;
var col := source.columns[keyColumn];
var keyName := col.Info.Name;
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;
var col := source.columns[keyColumn];
var keyName := col.Info.Name;
if col.Info.ColType = ctInt then
// добавление ключевого столбца
case col.Info.ColType of
ctInt:
begin
res.AddIntColumn(keyName, keys.Select(k -> integer(k)).ToArray, nil);
types.Add(ctInt);
end
else
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
@ -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,17 +4824,29 @@ begin
var col := source.columns[ci];
var colName := col.Info.Name;
if col.Info.ColType = ctInt then
case col.Info.ColType of
ctInt:
begin
res.AddIntColumn(colName, keys.Select(key -> integer(key.Values[k])).ToArray, nil);
types.Add(ctInt);
end
else
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);
cats.Add(true); // ключи categorical
end;
@ -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
keysN := groupsN.Keys.ToArray;
Error(ER_GROUPBY_UNSUPPORTED_KEY_TYPE);
end;
end
else
keysN := groupsN.Keys.OrderBy(k -> k).ToArray;
// ----------------------------
// 4. Аллокации только под нужные агрегаты
@ -5200,6 +5338,7 @@ begin
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
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,7 +5607,8 @@ begin
Result := df.ReplaceColumnFloat(colName, cur ->
begin
if not cur.IsValid(idx) then
Error(ER_INVALID_VALUE_IN_COLUMN, colName);
Result := real.NaN
else
Result := (cur.Float(idx) - mn) / (mx - mn);
end);
end;
@ -5451,6 +5616,8 @@ 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. заполняем данные
@ -5512,6 +5679,15 @@ begin
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
@ -6441,7 +6678,7 @@ begin
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;

View file

@ -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

View file

@ -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

View file

@ -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<string,string>;
static function ParseFeatures(meta: Dictionary<string,string>): 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<string,integer>;
@ -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<string,string>;
begin

View file

@ -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 FisherYates
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;
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
fUserProvidedSeed := false;
fRandomSeed := System.Environment.TickCount and integer.MaxValue;
end
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;
fUserProvidedSeed := true;
fRandomSeed := seed;
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;
// --- 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
@ -6142,6 +6008,13 @@ begin
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);
SoftmaxMatrix(logitsTrain, probsTrain);
@ -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;

View file

@ -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<IPreprocessor>;
//fMatrixSteps: List<ITransformer>;
@ -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,6 +625,8 @@ begin
// --- 3) Matrix transformers
X := FitTransformMatrix(X, y);
// После этого X уже не соответствует fFinalFeatures по размерности,
// но это нормально: fFinalFeatures используется только до ToMatrix.
// --- 4) модель
fModel := fModel.Fit(X, y);
@ -672,15 +688,7 @@ begin
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
@ -704,15 +712,7 @@ begin
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);
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.

View file

@ -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 может быть медленным.
/// Для больших выборок рекомендуется использовать альтернативную реализацию
/// через таблицу сопряжённости (contingency table) с асимптотикой O(n · k)
static function Metrics.AdjustedRandIndex(yTrue, yPred: Vector): real;
begin
if yTrue.Length <> yPred.Length then

View file

@ -785,6 +785,7 @@ begin
var container := rootChart.Content as GridWPF;
if container <> nil then
container.Children.Clear;
rootPaletteIndex := 0;
end);
end;

View file

@ -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);
k := ir;
end
else
Error(ER_IMPUTER_CONSTANT_TYPE_MISMATCH, name);
end;
res := res.ReplaceColumnInt(
name,
@ -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.

View file

@ -109,6 +109,8 @@ const
ER_DATASET_TOO_SMALL =
'Для {0} требуется как минимум 2 объекта!!' +
'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;