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; function GetColumn(i: integer): Column;
/// Добавляет в DataFrame столбец-представление (view), /// Добавляет в DataFrame столбец-синоним,
/// использующий те же данные, что и исходный столбец /// разделяющий те же массивы данных и валидности с исходным столбцом.
procedure AddColumnView(src: Column); /// Изменение данных повлияет на все DataFrame, использующие этот столбец.
procedure AddColumnAlias(src: Column);
function ExtendSchema(name: string; colType: ColumnType; isCategorical: boolean): DataFrameSchema; 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); procedure AddBoolColumn(name: string; data: array of boolean; valid: array of boolean := nil);
/// Возвращает данные целочисленного столбца по имени /// Возвращает данные целочисленного столбца по имени
/// Не изменяйте их, иначе DataFrame будет повреждён
function GetIntColumn(name: string): array of integer; function GetIntColumn(name: string): array of integer;
/// Возвращает данные вещественного столбца по имени /// Возвращает данные вещественного столбца по имени
/// Не изменяйте их, иначе DataFrame будет повреждён
function GetFloatColumn(name: string): array of real; function GetFloatColumn(name: string): array of real;
/// Возвращает данные строкового столбца по имени /// Возвращает данные строкового столбца по имени
/// Не изменяйте их, иначе DataFrame будет повреждён
function GetStrColumn(name: string): array of string; function GetStrColumn(name: string): array of string;
/// Возвращает данные логического столбца по имени /// Возвращает данные логического столбца по имени
/// Не изменяйте их, иначе DataFrame будет повреждён
function GetBoolColumn(name: string): array of boolean; function GetBoolColumn(name: string): array of boolean;
/// Вычисляет сумму значений столбца по индексу /// Вычисляет сумму значений столбца по индексу
@ -224,13 +229,24 @@ type
/// Возвращает статистику по всем числовым столбцам /// Возвращает статистику по всем числовым столбцам
function DescribeAll: Dictionary<string, DescribeStats>; function DescribeAll: Dictionary<string, DescribeStats>;
/// Группирует данные по столбцу по индексу /// Группирует данные по столбцу по индексу.
/// Поддерживаемые типы ключей: integer, string, boolean.
/// Вещественные столбцы (float) не поддерживаются из-за численной нестабильности.
function GroupBy(colIndex: integer): IGroupByContext; function GroupBy(colIndex: integer): IGroupByContext;
/// Группирует данные по столбцу по имени
/// Группирует данные по столбцу по имени.
/// Поддерживаемые типы ключей: integer, string, boolean.
/// Вещественные столбцы (float) не поддерживаются из-за численной нестабильности.
function GroupBy(colName: string): IGroupByContext; function GroupBy(colName: string): IGroupByContext;
/// Группирует данные по нескольким столбцам по индексам
/// Группирует данные по нескольким столбцам по индексам.
/// Все столбцы-ключи должны иметь тип integer, string или boolean.
/// Использование float-столбцов в качестве ключей не поддерживается.
function GroupBy(colIndices: array of integer): IGroupByContext; function GroupBy(colIndices: array of integer): IGroupByContext;
/// Группирует данные по нескольким столбцам по именам
/// Группирует данные по нескольким столбцам по именам.
/// Все столбцы-ключи должны иметь тип integer, string или boolean.
/// Использование float-столбцов в качестве ключей не поддерживается.
function GroupBy(colNames: array of string): IGroupByContext; function GroupBy(colNames: array of string): IGroupByContext;
/// Возвращает первые n строк /// Возвращает первые n строк
@ -585,9 +601,11 @@ const
'Индекс строки вне диапазона!!Row index is out of range'; 'Индекс строки вне диапазона!!Row index is out of range';
ER_UNSUPPORTED_COLUMN_TYPE = ER_UNSUPPORTED_COLUMN_TYPE =
'Неподдерживаемый тип столбца!!Unsupported column type'; 'Неподдерживаемый тип столбца!!Unsupported column type';
ER_JOIN_FLOAT_KEY_NOT_SUPPORTED =
'Соединение по вещественным ключам не поддерживается из-за ошибок точности!!Join on float keys is not supported due to precision issues';
type type
GroupKey = class GroupKey = class(IComparable<GroupKey>)
private private
fValues: array of object; fValues: array of object;
public public
@ -596,6 +614,8 @@ type
function GetHashCode: integer; override; function GetHashCode: integer; override;
property Values: array of object read fValues; property Values: array of object read fValues;
function CompareTo(other: GroupKey): integer;
end; end;
/// Класс для группировки данных /// Класс для группировки данных
@ -1155,7 +1175,10 @@ end;
function DataFrame.LeftJoinSingleKeyFloat(other: DataFrame; leftKey, rightKey: integer): DataFrame; function DataFrame.LeftJoinSingleKeyFloat(other: DataFrame; leftKey, rightKey: integer): DataFrame;
begin 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; var rcur := other.GetCursor;
while rcur.MoveNext do while rcur.MoveNext do
@ -1203,7 +1226,7 @@ begin
end; end;
end; end;
Result := BuildLeftJoinResult(Self, other, leftIdx, rightIdx, leftKey, rightKey); Result := BuildLeftJoinResult(Self, other, leftIdx, rightIdx, leftKey, rightKey);}
end; end;
function DataFrame.LeftJoinSingleKeyStr(other: DataFrame; leftKey, rightKey: integer): DataFrame; function DataFrame.LeftJoinSingleKeyStr(other: DataFrame; leftKey, rightKey: integer): DataFrame;
@ -1798,7 +1821,9 @@ end;
function DataFrame.JoinInnerSingleKeyFloat(other: DataFrame; leftKey, rightKey: integer): DataFrame; function DataFrame.JoinInnerSingleKeyFloat(other: DataFrame; leftKey, rightKey: integer): DataFrame;
begin 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; var rcur := other.GetCursor;
while rcur.MoveNext do while rcur.MoveNext do
@ -1834,7 +1859,7 @@ begin
end; end;
end; end;
Result := BuildJoinResult(Self, other, leftIdx, rightIdx, leftKey, rightKey); Result := BuildJoinResult(Self, other, leftIdx, rightIdx, leftKey, rightKey);}
end; end;
function DataFrame.JoinInnerSingleKeyStr(other: DataFrame; leftKey, rightKey: integer): DataFrame; function DataFrame.JoinInnerSingleKeyStr(other: DataFrame; leftKey, rightKey: integer): DataFrame;
@ -2057,7 +2082,7 @@ begin
Result := Self.Join(tmp, leftKeys, kind); Result := Self.Join(tmp, leftKeys, kind);
AssertSchemaConsistent; Result.AssertSchemaConsistent;
end; end;
procedure DataFrame.RebuildSchema; procedure DataFrame.RebuildSchema;
@ -2536,7 +2561,9 @@ begin
acc += d * d; acc += d * d;
end; end;
var variance := acc / cnt; var variance :=
if cnt > 1 then acc / (cnt - 1)
else 0.0;
Result := (mean, variance); Result := (mean, variance);
end; end;
@ -2601,7 +2628,12 @@ begin
Result.Count := cnt; Result.Count := cnt;
Result.Mean := mean; 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.Min := mn;
Result.Max := mx; Result.Max := mx;
end; end;
@ -2778,7 +2810,7 @@ begin
var cmp: integer; var cmp: integer;
case columns[colIndices[i]].Info.ColType of 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]); ctFloat: cmp := a.FloatVals[i].CompareTo(b.FloatVals[i]);
ctStr: cmp := a.StrVals[i].CompareTo(b.StrVals[i]); ctStr: cmp := a.StrVals[i].CompareTo(b.StrVals[i]);
ctBool: cmp := a.BoolVals[i].CompareTo(b.BoolVals[i]); ctBool: cmp := a.BoolVals[i].CompareTo(b.BoolVals[i]);
@ -2982,7 +3014,7 @@ begin
)); ));
Result := res; Result := res;
AssertSchemaConsistent; Result.AssertSchemaConsistent;
end; end;
function DataFrame.Select(colIndices: array of integer): DataFrame; function DataFrame.Select(colIndices: array of integer): DataFrame;
@ -3090,7 +3122,7 @@ begin
Result := res; Result := res;
AssertSchemaConsistent; Result.AssertSchemaConsistent;
end; end;
function DataFrame.Drop(colIndices: array of integer): DataFrame; function DataFrame.Drop(colIndices: array of integer): DataFrame;
@ -3267,7 +3299,7 @@ begin
res.SetSchema(ExtendSchema(name, ctInt, false)); res.SetSchema(ExtendSchema(name, ctInt, false));
Result := res; Result := res;
AssertSchemaConsistent; Result.AssertSchemaConsistent;
end; end;
function DataFrame.WithColumnFloat(name: string; f: DataFrameCursor -> real): DataFrame; function DataFrame.WithColumnFloat(name: string; f: DataFrameCursor -> real): DataFrame;
@ -3308,7 +3340,7 @@ begin
res.SetSchema(ExtendSchema(name, ctFloat, false)); res.SetSchema(ExtendSchema(name, ctFloat, false));
Result := res; Result := res;
AssertSchemaConsistent; Result.AssertSchemaConsistent;
end; end;
function DataFrame.WithColumnStr(name: string; f: DataFrameCursor -> string): DataFrame; function DataFrame.WithColumnStr(name: string; f: DataFrameCursor -> string): DataFrame;
@ -3349,7 +3381,7 @@ begin
res.SetSchema(ExtendSchema(name, ctStr, false)); res.SetSchema(ExtendSchema(name, ctStr, false));
Result := res; Result := res;
AssertSchemaConsistent; Result.AssertSchemaConsistent;
end; end;
function DataFrame.WithColumnBool(name: string; f: DataFrameCursor -> boolean): DataFrame; function DataFrame.WithColumnBool(name: string; f: DataFrameCursor -> boolean): DataFrame;
@ -3390,7 +3422,7 @@ begin
res.SetSchema(ExtendSchema(name, ctBool, false)); res.SetSchema(ExtendSchema(name, ctBool, false));
Result := res; Result := res;
AssertSchemaConsistent; Result.AssertSchemaConsistent;
end; end;
function DataFrame.ExtendSchema( function DataFrame.ExtendSchema(
@ -3482,9 +3514,21 @@ begin
for var i := 0 to columns.Count - 1 do for var i := 0 to columns.Count - 1 do
if i <> colIndex then if i <> colIndex then
res.AddColumnView(columns[i]) res.AddColumnAlias(columns[i])
else else
res.AddFloatColumn(colName, data, valid); 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 := res;
Result.AssertSchemaConsistent; Result.AssertSchemaConsistent;
@ -3523,10 +3567,22 @@ begin
var res := new DataFrame; var res := new DataFrame;
for var i := 0 to columns.Count - 1 do for var i := 0 to columns.Count - 1 do
if i <> colIndex then if i <> colIndex then
res.AddColumnView(columns[i]) res.AddColumnAlias(columns[i])
else else
res.AddIntColumn(colName, data, valid); 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 := res;
Result.AssertSchemaConsistent; Result.AssertSchemaConsistent;
end; end;
@ -3567,7 +3623,7 @@ begin
var res := new DataFrame; var res := new DataFrame;
for var i := 0 to columns.Count - 1 do for var i := 0 to columns.Count - 1 do
res.AddColumnView(columns[i]); res.AddColumnAlias(columns[i]);
res.AddIntColumn(name, data, valid); res.AddIntColumn(name, data, valid);
@ -3622,12 +3678,9 @@ begin
end; end;
end; end;
// КЛЮЧЕВОЕ: schema НЕ меняется
res.SetSchema(fSchema);
Result := res; Result := res;
AssertSchemaConsistent; Result.AssertSchemaConsistent;
end; end;
function DataFrame.TransformFloatColumn(name: string; f: real -> real): DataFrame; function DataFrame.TransformFloatColumn(name: string; f: real -> real): DataFrame;
@ -3677,12 +3730,9 @@ begin
end; end;
end; end;
// schema просто копируется
res.SetSchema(fSchema);
Result := res; Result := res;
AssertSchemaConsistent; Result.AssertSchemaConsistent;
end; end;
function DataFrame.TransformStrColumn(name: string; f: string -> string): DataFrame; function DataFrame.TransformStrColumn(name: string; f: string -> string): DataFrame;
@ -3732,12 +3782,9 @@ begin
end; end;
end; end;
// schema НЕ меняется
res.SetSchema(fSchema);
Result := res; Result := res;
AssertSchemaConsistent; Result.AssertSchemaConsistent;
end; end;
function DataFrame.TransformBoolColumn(name: string; f: boolean -> boolean): DataFrame; function DataFrame.TransformBoolColumn(name: string; f: boolean -> boolean): DataFrame;
@ -3787,12 +3834,9 @@ begin
end; end;
end; end;
// schema просто копируется
res.SetSchema(fSchema);
Result := res; Result := res;
AssertSchemaConsistent; Result.AssertSchemaConsistent;
end; end;
function DataFrame.TakeRows(indices: array of integer): DataFrame; function DataFrame.TakeRows(indices: array of integer): DataFrame;
@ -4256,7 +4300,7 @@ end;
/// Добавляет в DataFrame столбец-представление (view), /// Добавляет в DataFrame столбец-представление (view),
/// использующий те же данные, что и исходный столбец /// использующий те же данные, что и исходный столбец
procedure DataFrame.AddColumnView(src: Column); procedure DataFrame.AddColumnAlias(src: Column);
begin begin
case src.Info.ColType of case src.Info.ColType of
ctInt: ctInt:
@ -4309,7 +4353,7 @@ begin
res.AddIntColumn(name, data, fc.IsValid); res.AddIntColumn(name, data, fc.IsValid);
end end
else else
res.AddColumnView(col); res.AddColumnAlias(col);
end; end;
// пересобираем schema (меняются ТИПЫ) // пересобираем schema (меняются ТИПЫ)
@ -4423,6 +4467,31 @@ begin
Result := h; Result := h;
end; 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 // GroupView
//----------------------------- //-----------------------------
@ -4621,6 +4690,7 @@ begin
case df.columns[keyColumn].Info.ColType of case df.columns[keyColumn].Info.ColType of
ctInt: key := cursor.Int(keyColumn); ctInt: key := cursor.Int(keyColumn);
ctStr: key := cursor.Str(keyColumn); ctStr: key := cursor.Str(keyColumn);
ctBool: key := cursor.Bool(keyColumn);
else Error(ER_GROUPBY_UNSUPPORTED_KEY_TYPE, df.columns[keyColumn].Info.ColType); else Error(ER_GROUPBY_UNSUPPORTED_KEY_TYPE, df.columns[keyColumn].Info.ColType);
end; end;
@ -4658,6 +4728,7 @@ begin
case df.columns[c].Info.ColType of case df.columns[c].Info.ColType of
ctInt: values[i] := cursor.Int(c); ctInt: values[i] := cursor.Int(c);
ctStr: values[i] := cursor.Str(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); else Error(ER_GROUPBY_UNSUPPORTED_KEY_TYPE, df.columns[c].Info.ColType);
end; end;
end; end;
@ -4689,29 +4760,51 @@ begin
if singleKey then if singleKey then
begin 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 col := source.columns[keyColumn];
var keyName := col.Info.Name; var keyName := col.Info.Name;
if col.Info.ColType = ctInt then var keys: array of object;
begin
res.AddIntColumn(keyName, keys.Select(k -> integer(k)).ToArray, nil); case col.Info.ColType of
types.Add(ctInt); ctInt:
end keys := groups1.Keys.OrderBy(k -> integer(k)).Select(k -> object(k)).ToArray;
else ctStr:
begin keys := groups1.Keys.OrderBy(k -> string(k)).Select(k -> object(k)).ToArray;
res.AddStrColumn(keyName, keys.Select(k -> string(k)).ToArray, nil); ctBool:
types.Add(ctStr); keys := groups1.Keys.OrderBy(k -> boolean(k)).Select(k -> object(k)).ToArray;
else
Error(ER_GROUPBY_UNSUPPORTED_KEY_TYPE);
end; 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); names.Add(keyName);
cats.Add(true); // ключ — categorical cats.Add(true); // ключ — categorical
res.AddIntColumn('count', counts, nil); res.AddIntColumn('count', counts, nil);
names.Add('count'); names.Add('count');
types.Add(ctInt); types.Add(ctInt);
@ -4719,7 +4812,7 @@ begin
end end
else else
begin begin
var keys := groupsN.Select(kvp -> kvp.Key).ToArray; var keys := groupsN.Keys.OrderBy(k -> k).ToArray;
var counts := new integer[keys.Length]; var counts := new integer[keys.Length];
for var i := 0 to keys.Length - 1 do for var i := 0 to keys.Length - 1 do
@ -4731,15 +4824,27 @@ begin
var col := source.columns[ci]; var col := source.columns[ci];
var colName := col.Info.Name; var colName := col.Info.Name;
if col.Info.ColType = ctInt then case col.Info.ColType of
begin ctInt:
res.AddIntColumn(colName, keys.Select(key -> integer(key.Values[k])).ToArray, nil); begin
types.Add(ctInt); res.AddIntColumn(colName, keys.Select(key -> integer(key.Values[k])).ToArray, nil);
end types.Add(ctInt);
else end;
begin
res.AddStrColumn(colName, keys.Select(key -> string(key.Values[k])).ToArray, nil); ctStr:
types.Add(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; end;
names.Add(colName); names.Add(colName);
@ -4821,7 +4926,15 @@ begin
if singleKey then if singleKey then
begin 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 for var i := 0 to keys.Length - 1 do
begin begin
@ -4834,7 +4947,7 @@ begin
end end
else else
begin 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 for var i := 0 to keys.Length - 1 do
begin begin
@ -4961,9 +5074,34 @@ begin
var keysN: array of GroupKey := nil; var keysN: array of GroupKey := nil;
if singleKey then 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 else
keysN := groupsN.Keys.ToArray; keysN := groupsN.Keys.OrderBy(k -> k).ToArray;
// ---------------------------- // ----------------------------
// 4. Аллокации только под нужные агрегаты // 4. Аллокации только под нужные агрегаты
@ -5199,7 +5337,8 @@ begin
types.ToArray, types.ToArray,
cats.ToArray cats.ToArray
)); ));
res.AssertSchemaConsistent;
Result := res; Result := res;
end; end;
@ -5261,6 +5400,7 @@ begin
end; end;
end; end;
res.AssertSchemaConsistent;
Result := res; Result := res;
end; end;
@ -5273,29 +5413,47 @@ begin
var ix := df.ColumnIndex(colX); var ix := df.ColumnIndex(colX);
var iy := df.ColumnIndex(colY); 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 cur := df.GetCursor;
var sum := 0.0; var sumX := 0.0;
var sumY := 0.0;
var cnt := 0; var cnt := 0;
// pass 1: mean по пересечению
while cur.MoveNext do while cur.MoveNext do
if cur.IsValid(ix) and cur.IsValid(iy) then if cur.IsValid(ix) and cur.IsValid(iy) then
begin begin
sum += (cur.Float(ix) - mx) * (cur.Float(iy) - my); sumX += cur.Float(ix);
sumY += cur.Float(iy);
cnt += 1; cnt += 1;
end; end;
if cnt = 0 then if cnt = 0 then
Error(ER_NO_VALID_PAIRS); 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; end;
static function Statistics.CorrelationMatrix(df: DataFrame): DataFrame; static function Statistics.CorrelationMatrix(df: DataFrame): DataFrame;
@ -5333,7 +5491,12 @@ begin
if i = j then if i = j then
data[i] := 1.0 data[i] := 1.0
else 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); res.AddFloatColumn(names[j], data, nil);
@ -5355,8 +5518,9 @@ end;
static function Statistics.Standardize(df: DataFrame; colName: string): DataFrame; static function Statistics.Standardize(df: DataFrame; colName: string): DataFrame;
begin begin
var idx := df.ColumnIndex(colName); 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 if std = 0 then
Error(ER_ZERO_STD_STANDARDIZE); Error(ER_ZERO_STD_STANDARDIZE);
@ -5369,6 +5533,8 @@ end;
static function Statistics.StandardizeAll(df: DataFrame): DataFrame; static function Statistics.StandardizeAll(df: DataFrame): DataFrame;
begin begin
var res := new DataFrame; var res := new DataFrame;
var oldSchema := df.Schema;
var cur := df.GetCursor; var cur := df.GetCursor;
// 1. заранее считаем mean/std для всех числовых столбцов // 1. заранее считаем mean/std для всех числовых столбцов
@ -5381,8 +5547,9 @@ begin
var t := df.GetColumnType(i); var t := df.GetColumnType(i);
if t in [ColumnType.ctInt, ColumnType.ctFloat] then if t in [ColumnType.ctInt, ColumnType.ctFloat] then
begin begin
means[i] := df.Mean(i); var (mean, variance) := df.MeanVariance(i);
stds[i] := df.Std(i); means[i] := mean;
stds[i] := Sqrt(variance);
if stds[i] = 0 then if stds[i] = 0 then
Error(ER_ZERO_STD_COLUMN, df.fSchema.ColumnNames[i]); Error(ER_ZERO_STD_COLUMN, df.fSchema.ColumnNames[i]);
isNumeric[i] := true; isNumeric[i] := true;
@ -5395,7 +5562,7 @@ begin
if isNumeric[i] then if isNumeric[i] then
res.AddFloatColumn(df.fSchema.ColumnNames[i], new real[df.RowCount], nil) res.AddFloatColumn(df.fSchema.ColumnNames[i], new real[df.RowCount], nil)
else else
res.AddColumnView(df.columns[i]); // private helper res.AddColumnAlias(df.GetColumn(i));
end; end;
// 3. заполняем данные // 3. заполняем данные
@ -5409,26 +5576,23 @@ begin
var col := FloatColumn(res.columns[i]); var col := FloatColumn(res.columns[i]);
if cur.IsValid(i) then if cur.IsValid(i) then
begin col.Data[row] := (cur.Float(i) - means[i]) / stds[i]
col.Data[row] := (cur.Float(i) - means[i]) / stds[i];
if col.IsValid <> nil then
col.IsValid[row] := true;
end
else 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; col.IsValid[row] := false;
end;
end; end;
row += 1; row += 1;
end; 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; Result := res;
end; end;
@ -5443,14 +5607,17 @@ begin
Result := df.ReplaceColumnFloat(colName, cur -> Result := df.ReplaceColumnFloat(colName, cur ->
begin begin
if not cur.IsValid(idx) then if not cur.IsValid(idx) then
Error(ER_INVALID_VALUE_IN_COLUMN, colName); Result := real.NaN
Result := (cur.Float(idx) - mn) / (mx - mn); else
Result := (cur.Float(idx) - mn) / (mx - mn);
end); end);
end; end;
static function Statistics.NormalizeAll(df: DataFrame): DataFrame; static function Statistics.NormalizeAll(df: DataFrame): DataFrame;
begin begin
var res := new DataFrame; var res := new DataFrame;
var oldSchema := df.Schema;
var cur := df.GetCursor; var cur := df.GetCursor;
// 1. заранее считаем min/max для всех числовых столбцов // 1. заранее считаем min/max для всех числовых столбцов
@ -5478,7 +5645,7 @@ begin
if isNumeric[i] then if isNumeric[i] then
res.AddFloatColumn(df.fSchema.ColumnNames[i], new real[df.RowCount], nil) res.AddFloatColumn(df.fSchema.ColumnNames[i], new real[df.RowCount], nil)
else else
res.AddColumnView(df.columns[i]); // private helper res.AddColumnAlias(df.GetColumn(i));
end; end;
// 3. заполняем данные // 3. заполняем данные
@ -5511,6 +5678,15 @@ begin
end; end;
row += 1; row += 1;
end; 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; Result := res;
end; end;
@ -5940,13 +6116,49 @@ begin
var autoCat: array of boolean := nil; var autoCat: array of boolean := nil;
var first := true; 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 foreach var line in linesArray index inferRead do
begin begin
if inferRead >= inferLimit then break; if inferRead >= inferLimit then break;
if first then if first then
begin 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 if hasHeader then
begin begin
@ -6046,7 +6258,24 @@ begin
if hasHeader then continue; if hasHeader then continue;
end; 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 parts.Length <> originalColCount then
if strict then if strict then
@ -6164,9 +6393,6 @@ begin
strData[j] := new string[rowCount]; strData[j] := new string[rowCount];
end; end;
var starts := new integer[originalColCount];
var lens := new integer[originalColCount];
var row := 0; var row := 0;
first := true; first := true;
foreach var line in linesArray do foreach var line in linesArray do
@ -6177,9 +6403,6 @@ begin
if hasHeader then continue; if hasHeader then continue;
end; end;
var actualCount: integer;
var unclosedQuote: boolean;
ScanFieldsQuoted(line, delimiter, starts, lens, actualCount, unclosedQuote); ScanFieldsQuoted(line, delimiter, starts, lens, actualCount, unclosedQuote);
if unclosedQuote then if unclosedQuote then
@ -6423,6 +6646,20 @@ begin
); );
end; 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; static procedure CsvSaver.Save(df: DataFrame; filename: string;
delimiter: char; header: boolean); delimiter: char; header: boolean);
begin begin
@ -6438,10 +6675,10 @@ begin
// header // header
if header then if header then
begin begin
for var i := 0 to n-1 do for var i := 0 to n - 1 do
begin begin
if i > 0 then w.Write(delimiter); if i > 0 then w.Write(delimiter);
w.Write(schema.NameAt(i)); w.Write(EscapeCsv(schema.NameAt(i), delimiter));
end; end;
w.WriteLine; w.WriteLine;
end; end;
@ -6460,7 +6697,7 @@ begin
case schema.ColumnTypeAt(i) of case schema.ColumnTypeAt(i) of
ctInt: w.Write(cur.Int(i)); ctInt: w.Write(cur.Int(i));
ctFloat: w.Write(cur.Float(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)); ctBool: w.Write(cur.Bool(i));
end; end;
end; end;

View file

@ -557,7 +557,6 @@ begin
IsValid := valid; IsValid := valid;
end; end;
constructor IntColumn.Create(name: string); constructor IntColumn.Create(name: string);
begin begin
inherited Create; inherited Create;
@ -567,34 +566,9 @@ begin
IsValid := nil; IsValid := nil;
end; 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; function IntColumn.TryGetNumericValue(i: integer; var value: real): boolean;
begin begin
if not IsValid[i] then if (IsValid <> nil) and not IsValid[i] then
exit(false); exit(false);
value := Data[i]; value := Data[i];
@ -620,34 +594,9 @@ begin
end; 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; function FloatColumn.TryGetNumericValue(i: integer; var value: real): boolean;
begin begin
if not IsValid[i] then if (IsValid <> nil) and not IsValid[i] then
exit(false); exit(false);
value := Data[i]; value := Data[i];
@ -673,31 +622,6 @@ begin
IsValid := nil; IsValid := nil;
end; 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; function StrColumn.TryGetNumericValue(i: integer; var value: real): boolean;
begin begin
exit(false); exit(false);
@ -705,7 +629,7 @@ end;
function BoolColumn.TryGetNumericValue(i: integer; var value: real): boolean; function BoolColumn.TryGetNumericValue(i: integer; var value: real): boolean;
begin begin
if not IsValid[i] then if (IsValid <> nil) and not IsValid[i] then
exit(false); exit(false);
if Data[i] then if Data[i] then

View file

@ -325,6 +325,9 @@ const
'Для QR-разложения требуется m >= n!!QR decomposition requires m >= n'; 'Для QR-разложения требуется m >= n!!QR decomposition requires m >= n';
ER_SINGULAR_MATRIX = ER_SINGULAR_MATRIX =
'Матрица вырождена или плохо обусловлена!!Matrix is singular or ill-conditioned'; 'Матрица вырождена или плохо обусловлена!!Matrix is singular or ill-conditioned';
ER_EMPTY_MATRIX =
'Матрица пуста!!Matrix is empty';
type type
MLNotSPDException = class(MLException); MLNotSPDException = class(MLException);
@ -470,7 +473,7 @@ end;
function Vector.Average: real; function Vector.Average: real;
begin begin
if Length = 0 then if Length = 0 then
ArgumentError('Vector is empty'); ArgumentError(ER_VECTOR_EMPTY);
Result := Sum / Length; Result := Sum / Length;
end; end;
@ -646,6 +649,10 @@ function Matrix.ColumnMins: Vector;
begin begin
var n := RowCount; var n := RowCount;
var p := ColCount; var p := ColCount;
if n = 0 then
exit(new Vector(p));
Result := new Vector(p); Result := new Vector(p);
for var j := 0 to p - 1 do for var j := 0 to p - 1 do
@ -662,6 +669,10 @@ function Matrix.ColumnMaxs: Vector;
begin begin
var n := RowCount; var n := RowCount;
var p := ColCount; var p := ColCount;
if n = 0 then
exit(new Vector(p));
Result := new Vector(p); Result := new Vector(p);
for var j := 0 to p - 1 do for var j := 0 to p - 1 do
@ -678,6 +689,10 @@ function Matrix.RowMins: Vector;
begin begin
var n := RowCount; var n := RowCount;
var p := ColCount; var p := ColCount;
if n = 0 then
exit(new Vector(p));
Result := new Vector(n); Result := new Vector(n);
for var i := 0 to n - 1 do for var i := 0 to n - 1 do
@ -694,6 +709,10 @@ function Matrix.RowMaxs: Vector;
begin begin
var n := RowCount; var n := RowCount;
var p := ColCount; var p := ColCount;
if n = 0 then
exit(new Vector(p));
Result := new Vector(n); Result := new Vector(n);
for var i := 0 to n - 1 do for var i := 0 to n - 1 do
@ -782,10 +801,15 @@ end;
function Matrix.ColumnArgMin(j: integer): integer; function Matrix.ColumnArgMin(j: integer): integer;
begin begin
var n := RowCount;
if n = 0 then
Error(ER_EMPTY_MATRIX);
var minVal := fdata[0,j]; var minVal := fdata[0,j];
var arg := 0; 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 if fdata[i,j] < minVal then
begin begin
minVal := fdata[i,j]; minVal := fdata[i,j];
@ -802,10 +826,15 @@ end;
function Matrix.ColumnArgMax(j: integer): integer; function Matrix.ColumnArgMax(j: integer): integer;
begin begin
var n := RowCount;
if n = 0 then
Error(ER_EMPTY_MATRIX);
var maxVal := fdata[0,j]; var maxVal := fdata[0,j];
var arg := 0; 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 if fdata[i,j] > maxVal then
begin begin
maxVal := fdata[i,j]; maxVal := fdata[i,j];
@ -1388,7 +1417,7 @@ begin
maxRow := i; maxRow := i;
end; end;
if maxVal = 0.0 then if maxVal < 1e-12 then
Error(ER_MATRIX_SINGULAR); Error(ER_MATRIX_SINGULAR);
if maxRow <> k then if maxRow <> k then
@ -1604,6 +1633,9 @@ begin
for var i := k to m - 1 do for var i := k to m - 1 do
vnorm2 += R[i,k] * R[i,k]; vnorm2 += R[i,k] * R[i,k];
if vnorm2 < 1e-12 then
continue;
var beta := 2.0 / vnorm2; var beta := 2.0 / vnorm2;
// ---- применить отражение к R // ---- применить отражение к R

View file

@ -259,7 +259,7 @@ type
/// Датасет российских городов (задача кластеризации) /// Датасет российских городов (задача кластеризации)
static function RussianCities: Dataset; static function RussianCities: Dataset;
/// Датасет результатов экзамена студентов (классификация) {/// Датасет результатов экзамена студентов (классификация)
static function StudentExam: Dataset; static function StudentExam: Dataset;
/// Датасет банковских клиентов (классификация одобрения кредита) /// Датасет банковских клиентов (классификация одобрения кредита)
@ -272,7 +272,7 @@ type
static function MoscowTransport: Dataset; static function MoscowTransport: Dataset;
/// Датасет интернет-покупок пользователей (классификация покупки) /// Датасет интернет-покупок пользователей (классификация покупки)
static function OnlineShopping: Dataset; static function OnlineShopping: Dataset;}
static function LoadMeta(path: string): Dictionary<string,string>; static function LoadMeta(path: string): Dictionary<string,string>;
static function ParseFeatures(meta: Dictionary<string,string>): array of string; static function ParseFeatures(meta: Dictionary<string,string>): array of string;
@ -330,6 +330,11 @@ const
'Неподдерживаемый тип ключа для группировки!!Unsupported key type for grouping'; 'Неподдерживаемый тип ключа для группировки!!Unsupported key type for grouping';
ER_STRATIFIED_ONLY_FOR_CLASSIFICATION = ER_STRATIFIED_ONLY_FOR_CLASSIFICATION =
'Стратифицированное разбиение доступно только для задач классификации!!Stratified split is only for classification tasks'; 'Стратифицированное разбиение доступно только для задач классификации!!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_DATASET = 'Датасет: {0}!!Dataset: {0}';
C_DESCRIPTION = 'Описание:!!Description:'; C_DESCRIPTION = 'Описание:!!Description:';
@ -593,7 +598,14 @@ begin
if Task <> TaskType.Classification then if Task <> TaskType.Classification then
ArgumentError(ER_CLASSES_ONLY_CLASSIFICATION); 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; end;
function Dataset.ClassCounts: Dictionary<string,integer>; function Dataset.ClassCounts: Dictionary<string,integer>;
@ -678,6 +690,9 @@ begin
if (classBalance <= 0) or (classBalance > 1) then if (classBalance <= 0) or (classBalance > 1) then
ArgumentOutOfRangeError(ER_PARAM_RANGE_01, 'classBalance'); ArgumentOutOfRangeError(ER_PARAM_RANGE_01, 'classBalance');
if classBalance < 1e-3 then
ArgumentOutOfRangeError(ER_CLASS_BALANCE_TOO_SMALL, classBalance);
if noisePoints < 0 then if noisePoints < 0 then
ArgumentOutOfRangeError(ER_PARAM_GE_ZERO, 'noisePoints'); ArgumentOutOfRangeError(ER_PARAM_GE_ZERO, 'noisePoints');
@ -1301,7 +1316,7 @@ begin
Result := ds; Result := ds;
end; end;
static function Datasets.StudentExam: Dataset; {static function Datasets.StudentExam: Dataset;
begin begin
NotImplementedError(ER_NOT_IMPLEMENTED, 'Datasets.StudentExam'); NotImplementedError(ER_NOT_IMPLEMENTED, 'Datasets.StudentExam');
Result := nil; Result := nil;
@ -1329,7 +1344,7 @@ static function Datasets.OnlineShopping: Dataset;
begin begin
NotImplementedError(ER_NOT_IMPLEMENTED, 'Datasets.OnlineShopping'); NotImplementedError(ER_NOT_IMPLEMENTED, 'Datasets.OnlineShopping');
Result := nil; Result := nil;
end; end;}
static function Datasets.LoadMeta(path: string): Dictionary<string,string>; static function Datasets.LoadMeta(path: string): Dictionary<string,string>;
begin begin

View file

@ -486,11 +486,13 @@ type
// DecisionTreeBase // DecisionTreeBase
//============================ //============================
/// Базовый абстрактный класс дерева решений. /// Базовый абстрактный класс дерева решений.
/// Используется только DecisionTreeRegressor
/// Classifier использует DecisionTreeCore
/// Реализует общую логику построения структуры дерева: /// Реализует общую логику построения структуры дерева:
/// рекурсивное разбиение, контроль глубины, /// рекурсивное разбиение, контроль глубины,
/// минимального числа объектов и расчет важности признаков. /// минимального числа объектов и расчет важности признаков.
/// Конкретная логика вычисления значения листа и критерия разбиения задается в наследниках /// Конкретная логика вычисления значения листа и критерия разбиения задается в наследнике
DecisionTreeBase = abstract class(ITreeModel) DecisionTreeRegressorBase = abstract class(ITreeModel)
protected protected
fRoot: DecisionTreeNode; fRoot: DecisionTreeNode;
fMaxDepth: integer; fMaxDepth: integer;
@ -516,7 +518,7 @@ type
function LeafValue(y: Vector; indices: array of integer): real; virtual; abstract; function LeafValue(y: Vector; indices: array of integer): real; virtual; abstract;
function LeafNode(value: real): DecisionTreeNode; function LeafNode(value: real): DecisionTreeNode;
procedure CopyBaseState(dest: DecisionTreeBase); procedure CopyBaseState(dest: DecisionTreeRegressorBase);
function GetFeatureSubset(nFeatures: integer): array of integer; virtual; function GetFeatureSubset(nFeatures: integer): array of integer; virtual;
procedure SetRowIndices(rows: array of integer); procedure SetRowIndices(rows: array of integer);
@ -563,20 +565,23 @@ type
/// Дерево решений для задачи классификации. /// Дерево решений для задачи классификации.
/// Использует критерий нечистоты (обычно Gini) для выбора оптимальных разбиений. /// Использует критерий нечистоты (обычно Gini) для выбора оптимальных разбиений.
/// В листьях хранится наиболее частый класс /// В листьях хранится наиболее частый класс
DecisionTreeClassifier = class(DecisionTreeBase, IClassifier) DecisionTreeClassifier = class(IClassifier)
private 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; fCore: DecisionTreeCore;
fIndexToClass: array of integer; fIndexToClass: array of integer;
fClassLabels: array of string; 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 public
/// Создает классификационное дерево: /// Создает классификационное дерево:
/// • maxDepth — максимальная глубина дерева (-1 означает без ограничения). /// • maxDepth — максимальная глубина дерева (-1 означает без ограничения).
@ -589,11 +594,11 @@ type
/// X — матрица признаков. /// X — матрица признаков.
/// y — вектор целевых меток (целые значения). /// y — вектор целевых меток (целые значения).
/// Строит структуру дерева путем минимизации нечистоты в узлах. /// Строит структуру дерева путем минимизации нечистоты в узлах.
function Fit(X: Matrix; y: Vector): ISupervisedModel; override; function Fit(X: Matrix; y: Vector): ISupervisedModel;
/// Выполняет предсказание меток классов для X. /// Выполняет предсказание меток классов для X.
/// Для каждого объекта возвращается класс, соответствующий листу дерева. /// Для каждого объекта возвращается класс, соответствующий листу дерева.
function Predict(X: Matrix): Vector; override; function Predict(X: Matrix): Vector;
/// Возвращает предсказанные метки классов для объектов из X. /// Возвращает предсказанные метки классов для объектов из X.
/// Каждый элемент результата — индекс класса (целое число). /// Каждый элемент результата — индекс класса (целое число).
@ -603,7 +608,7 @@ type
/// Создает глубокую копию дерева классификации. /// Создает глубокую копию дерева классификации.
/// Копируется структура узлов, параметры и обученное состояние. /// Копируется структура узлов, параметры и обученное состояние.
function Clone: IModel; override; function Clone: IModel;
/// Возвращает строковое представление модели. /// Возвращает строковое представление модели.
function ToString: string; override; function ToString: string; override;
@ -617,6 +622,10 @@ type
procedure SetClassLabels(classes: array of string); procedure SetClassLabels(classes: array of string);
function GetClassLabels: array of string; function GetClassLabels: array of string;
/// Возвращает true, если дерево обучено.
/// Если false — Predict вызовет ошибку.
property IsFitted: boolean read fFitted;
end; end;
//============================ //============================
@ -627,7 +636,7 @@ type
/// Использует критерий дисперсии для выбора разбиений. /// Использует критерий дисперсии для выбора разбиений.
/// В листьях хранится среднее значение целевой переменной. /// В листьях хранится среднее значение целевой переменной.
/// Поддерживает L2-регуляризацию значения листа (leafL2) /// Поддерживает L2-регуляризацию значения листа (leafL2)
DecisionTreeRegressor = class(DecisionTreeBase, IRegressor) DecisionTreeRegressor = class(DecisionTreeRegressorBase, IRegressor)
private private
fLeafL2: real; fLeafL2: real;
@ -2633,7 +2642,7 @@ end;
constructor LassoRegression.Create(alpha: real; maxIter: integer; tol: real); constructor LassoRegression.Create(alpha: real; maxIter: integer; tol: real);
begin begin
// Lasso = ElasticNet с L2 = 0 // Lasso = ElasticNet с L2 = 0
new ElasticNet(alpha, 0.0, maxIter, tol); fModel := new ElasticNet(alpha, 0.0, maxIter, tol);
end; end;
function LassoRegression.Fit(X: Matrix; y: Vector): ISupervisedModel; function LassoRegression.Fit(X: Matrix; y: Vector): ISupervisedModel;
@ -2810,7 +2819,7 @@ begin
// --- convergence check // --- convergence check
if fCheckConvergence then if fCheckConvergence then
begin begin
if Abs(prevLoss - loss) < fTol then if Abs(prevLoss - loss) < Max(fTol, fMinImprovement) then
break; break;
prevLoss := loss; prevLoss := loss;
@ -2934,7 +2943,7 @@ begin
Result := new integer[v.Length]; Result := new integer[v.Length];
for var i := 0 to v.Length - 1 do for var i := 0 to v.Length - 1 do
Result[i] := integer(v[i]); Result[i] := Round(v[i]);
end; end;
function LogisticRegression.ToString: string; function LogisticRegression.ToString: string;
@ -3490,7 +3499,7 @@ end;
// DecisionTreeBase // DecisionTreeBase
constructor DecisionTreeBase.Create( constructor DecisionTreeRegressorBase.Create(
maxDepth: integer; maxDepth: integer;
minSamplesSplit: integer; minSamplesSplit: integer;
minSamplesLeaf: integer; minSamplesLeaf: integer;
@ -3536,7 +3545,7 @@ begin
fRng := new System.Random(fRandomSeed); fRng := new System.Random(fRandomSeed);
end; end;
procedure DecisionTreeBase.CopyBaseState(dest: DecisionTreeBase); procedure DecisionTreeRegressorBase.CopyBaseState(dest: DecisionTreeRegressorBase);
begin begin
dest.fMaxDepth := fMaxDepth; dest.fMaxDepth := fMaxDepth;
dest.fMinSamplesSplit := fMinSamplesSplit; dest.fMinSamplesSplit := fMinSamplesSplit;
@ -3566,7 +3575,7 @@ begin
dest.fRowIndices := Copy(fRowIndices); dest.fRowIndices := Copy(fRowIndices);
end; end;
function DecisionTreeBase.GetFeatureSubset(nFeatures: integer): array of integer; function DecisionTreeRegressorBase.GetFeatureSubset(nFeatures: integer): array of integer;
begin begin
if (fMaxFeatures = 0) or (fMaxFeatures >= nFeatures) then if (fMaxFeatures = 0) or (fMaxFeatures >= nFeatures) then
begin begin
@ -3592,7 +3601,7 @@ begin
Result := subset; Result := subset;
end; end;
procedure DecisionTreeBase.SetRowIndices(rows: array of integer); procedure DecisionTreeRegressorBase.SetRowIndices(rows: array of integer);
begin begin
if Length(rows) = 0 then if Length(rows) = 0 then
ArgumentError('Row subset cannot be empty!!Row subset cannot be empty'); ArgumentError('Row subset cannot be empty!!Row subset cannot be empty');
@ -3600,7 +3609,7 @@ begin
fRowIndices := Copy(rows); fRowIndices := Copy(rows);
end; end;
function DecisionTreeBase.FeatureImportances: Vector; function DecisionTreeRegressorBase.FeatureImportances: Vector;
begin begin
if not fFitted then if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED); NotFittedError(ER_FIT_NOT_CALLED);
@ -3611,7 +3620,7 @@ begin
Result := fFeatureImportances.Clone; Result := fFeatureImportances.Clone;
end; end;
function DecisionTreeBase.LeafNode(value: real): DecisionTreeNode; function DecisionTreeRegressorBase.LeafNode(value: real): DecisionTreeNode;
begin begin
var n := new DecisionTreeNode; var n := new DecisionTreeNode;
n.IsLeaf := true; n.IsLeaf := true;
@ -3619,7 +3628,7 @@ begin
Result := n; Result := n;
end; end;
function DecisionTreeBase.BuildTree(X: Matrix; y: Vector; function DecisionTreeRegressorBase.BuildTree(X: Matrix; y: Vector;
indices: array of integer; depth: integer): DecisionTreeNode; indices: array of integer; depth: integer): DecisionTreeNode;
begin begin
if (fMaxDepth >= 0) and (depth >= fMaxDepth) then if (fMaxDepth >= 0) and (depth >= fMaxDepth) then
@ -3695,7 +3704,7 @@ end;
const EPS = 1e-12; 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 begin
Result := fCriterion.Impurity(y, indices) < EPS; Result := fCriterion.Impurity(y, indices) < EPS;
end; end;
@ -3823,180 +3832,37 @@ end;
// DecisionTreeClassifier // 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 begin
var n := indices.Length; if maxDepth = 0 then
if n <= 1 then ArgumentOutOfRangeError(ER_MAX_DEPTH_INVALID, maxDepth);
exit(SplitResult.Invalid);
var p := X.ColCount; if minSamplesSplit < 2 then
ArgumentOutOfRangeError(ER_MIN_SAMPLES_SPLIT_INVALID, minSamplesSplit);
var bestFeature := -1; if minSamplesLeaf < 1 then
var bestThreshold := 0.0; ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf);
var bestScore := real.PositiveInfinity;
var feat := new integer[p]; if minSamplesLeaf >= minSamplesSplit then
for var i := 0 to p - 1 do ArgumentOutOfRangeError(ER_MIN_LEAF_GE_SPLIT, minSamplesLeaf, minSamplesSplit);
feat[i] := i;
var m := fMaxFeatures; fMaxDepth := maxDepth;
if (m <= 0) or (m > p) then fMinSamplesSplit := minSamplesSplit;
m := p; fMinSamplesLeaf := minSamplesLeaf;
fCriterion := if criterion = nil then new GiniCriterion else criterion;
// partial Fisher–Yates if seed < 0 then
for var i := 0 to m - 1 do
begin begin
var j := fRng.Next(i, p); fUserProvidedSeed := false;
var tmp := feat[i]; fRandomSeed := System.Environment.TickCount and integer.MaxValue;
feat[i] := feat[j]; end
feat[j] := tmp; else
begin
fUserProvidedSeed := true;
fRandomSeed := seed;
end; end;
for var fi := 0 to m - 1 do fRng := new System.Random(fRandomSeed);
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);
end; end;
function DecisionTreeClassifier.Fit(X: Matrix; y: Vector): ISupervisedModel; function DecisionTreeClassifier.Fit(X: Matrix; y: Vector): ISupervisedModel;
@ -4060,7 +3926,8 @@ begin
fMinSamplesSplit, fMinSamplesSplit,
fMinSamplesLeaf, fMinSamplesLeaf,
fCriterion, fCriterion,
0 // maxFeatures = 0 -> использовать все признаки fMaxFeatures,
fRandomSeed
); );
fCore.Fit(X, yEncoded); fCore.Fit(X, yEncoded);
@ -4115,7 +3982,24 @@ begin
fRandomSeed 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 (единственный источник истины) // --- classes (единственный источник истины)
if fIndexToClass <> nil then if fIndexToClass <> nil then
@ -4134,20 +4018,16 @@ begin
fMinSamplesSplit, fMinSamplesSplit,
fMinSamplesLeaf, fMinSamplesLeaf,
fCriterion, fCriterion,
fMaxFeatures fMaxFeatures,
fRandomSeed
); );
m.fCore.fRoot := fCore.fRoot.Clone; // ключевой момент m.fCore.fRoot := fCore.fRoot.Clone;
end; end;
Result := m; Result := m;
end; end;
function DecisionTreeClassifier.LeafValue(y: Vector; indices: array of integer): real;
begin
Result := MajorityClass(y, indices);
end;
function DecisionTreeClassifier.ToString: string; function DecisionTreeClassifier.ToString: string;
begin begin
Result := Result :=
@ -4907,7 +4787,7 @@ begin
Result := new integer[v.Length]; Result := new integer[v.Length];
for var i := 0 to v.Length - 1 do for var i := 0 to v.Length - 1 do
Result[i] := integer(v[i]); Result[i] := Round(v[i]);
end; end;
function RandomForestClassifier.PredictProba(X: Matrix): Matrix; function RandomForestClassifier.PredictProba(X: Matrix): Matrix;
@ -5517,19 +5397,8 @@ begin
var deltaTrain := tree.Predict(XTrain); var deltaTrain := tree.Predict(XTrain);
// --- update TRAIN --- // --- update TRAIN ---
if useSubsample then for var i := 0 to nTrain - 1 do
begin yPredTrain[i] += fLearningRate * deltaTrain[i];
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 --- // --- update OOB ---
if useOOB then if useOOB then
@ -6128,10 +5997,7 @@ begin
begin begin
for var i := 0 to nTrain - 1 do for var i := 0 to nTrain - 1 do
if not inBag[i] then if not inBag[i] then
begin
logitsOOB[i, cls] += fLearningRate * deltaTrain[i]; logitsOOB[i, cls] += fLearningRate * deltaTrain[i];
oobCount[i] += 1;
end;
end; end;
if useValidation then if useValidation then
@ -6141,6 +6007,13 @@ begin
logitsVal[i, cls] += fLearningRate * deltaVal[i]; logitsVal[i, cls] += fLearningRate * deltaVal[i];
end; end;
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); fEstimators.Add(trees);
@ -6801,7 +6674,7 @@ begin
Result := new integer[v.Length]; Result := new integer[v.Length];
for var i := 0 to v.Length - 1 do for var i := 0 to v.Length - 1 do
Result[i] := integer(v[i]); Result[i] := Round(v[i]);
end; end;
procedure GradientBoostingClassifier.SetClassLabels(classes: array of string); procedure GradientBoostingClassifier.SetClassLabels(classes: array of string);
@ -7146,7 +7019,7 @@ begin
Result := new integer[v.Length]; Result := new integer[v.Length];
for var i := 0 to v.Length - 1 do for var i := 0 to v.Length - 1 do
Result[i] := integer(v[i]); Result[i] := Round(v[i]);
end; end;
function KNNClassifier.PredictProba(X: Matrix): Matrix; function KNNClassifier.PredictProba(X: Matrix): Matrix;

View file

@ -50,7 +50,7 @@ type
/// • target — целевая переменная. /// • target — целевая переменная.
/// ///
/// Если модель не добавлена, DataPipeline работает как чистый DF-конвейер (Fit/Transform/FitTransform) /// Если модель не добавлена, DataPipeline работает как чистый DF-конвейер (Fit/Transform/FitTransform)
DataPipeline = class(PipelineBase) DataPipeline = class(PipelineBase, IModel)
private private
fModel: ISupervisedModel; fModel: ISupervisedModel;
fTask: TaskKind; fTask: TaskKind;
@ -112,6 +112,10 @@ type
function GetClassLabels: array of string; function GetClassLabels: array of string;
function ToString: string; override; function ToString: string; override;
function Name: string := Self.GetType.Name;
function Clone: IModel;
end; end;
/// UDataPipeline — конвейер подготовки данных и обучения модели без учителя на DataFrame. /// UDataPipeline — конвейер подготовки данных и обучения модели без учителя на DataFrame.
@ -131,7 +135,7 @@ type
/// ///
/// Если модель не добавлена, UDataPipeline работает как чистый DF-конвейер /// Если модель не добавлена, UDataPipeline работает как чистый DF-конвейер
/// (Fit/Transform/FitTransform) /// (Fit/Transform/FitTransform)
UDataPipeline = class(PipelineBase) UDataPipeline = class(PipelineBase, IModel)
private private
//fDataSteps: List<IPreprocessor>; //fDataSteps: List<IPreprocessor>;
//fMatrixSteps: List<ITransformer>; //fMatrixSteps: List<ITransformer>;
@ -198,6 +202,10 @@ type
function PredictLabels(df: DataFrame): array of integer; function PredictLabels(df: DataFrame): array of integer;
function ToString: string; override; function ToString: string; override;
function Clone: IModel;
function Name: string := Self.GetType.Name;
end; end;
implementation implementation
@ -304,6 +312,8 @@ const
ER_MODEL_NOT_CLASSIFIER = ER_MODEL_NOT_CLASSIFIER =
'Модель не является классификатором!!' + 'Модель не является классификатором!!' +
'Model is not a classifier'; 'Model is not a classifier';
ER_MODEL_CLONE_TYPE =
'Clone модели вернул неподдерживаемый тип!!Model Clone returned unsupported type';
//----------------------------- //-----------------------------
// DataPipeline // DataPipeline
@ -588,6 +598,10 @@ begin
if feats.Count = 0 then if feats.Count = 0 then
ArgumentError(ER_PIPELINE_NO_FEATURES); ArgumentError(ER_PIPELINE_NO_FEATURES);
// fFinalFeatures — признаки DataFrame (ДО matrix-transform)
// После FitTransformMatrix пространство признаков меняется (PCA и т.п.),
// но fFinalFeatures используется только для ToMatrix в Predict.
fFinalFeatures := feats.ToArray; fFinalFeatures := feats.ToArray;
var X := current.ToMatrix(fFinalFeatures); var X := current.ToMatrix(fFinalFeatures);
@ -611,10 +625,12 @@ begin
// --- 3) Matrix transformers // --- 3) Matrix transformers
X := FitTransformMatrix(X, y); X := FitTransformMatrix(X, y);
// После этого X уже не соответствует fFinalFeatures по размерности,
// но это нормально: fFinalFeatures используется только до ToMatrix.
// --- 4) модель // --- 4) модель
fModel := fModel.Fit(X, y); fModel := fModel.Fit(X, y);
if fTask = tkClassification then if fTask = tkClassification then
begin begin
if fModel is IClassifier(var cls) then if fModel is IClassifier(var cls) then
@ -671,18 +687,10 @@ begin
ArgumentError(ER_MODEL_NULL); ArgumentError(ER_MODEL_NULL);
var current := Transform(df); 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); X := TransformMatrix(X);
if not (fModel is IPredictiveModel) then if not (fModel is IPredictiveModel) then
Error(ER_PREDICT_NOT_SUPPORTED); Error(ER_PREDICT_NOT_SUPPORTED);
@ -703,16 +711,8 @@ begin
ArgumentError(ER_PROBA_NOT_SUPPORTED); ArgumentError(ER_PROBA_NOT_SUPPORTED);
var current := Transform(df); 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); X := TransformMatrix(X);
Result := (fModel as IProbabilisticClassifier).PredictProba(X); Result := (fModel as IProbabilisticClassifier).PredictProba(X);
@ -807,6 +807,36 @@ begin
Result := sb; Result := sb;
end; 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 // UDataPipeline
//----------------------------- //-----------------------------
@ -1195,4 +1225,26 @@ begin
Result := sb; Result := sb;
end; 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. end.

View file

@ -994,7 +994,7 @@ begin
var correct := 0; var correct := 0;
for var i := 0 to n - 1 do for var i := 0 to n - 1 do
if yTrue[i] = yPred[i] then if Round(yTrue[i]) = Round(yPred[i]) then
correct += 1; correct += 1;
Result := correct / n; Result := correct / n;
@ -1667,6 +1667,9 @@ end;
/// 1 — идеальное совпадение кластеризации и истинных меток /// 1 — идеальное совпадение кластеризации и истинных меток
/// 0 — случайное разбиение /// 0 — случайное разбиение
/// <0 — хуже случайного /// <0 — хуже случайного
/// Сложность: O(n²). При больших n может быть медленным.
/// Для больших выборок рекомендуется использовать альтернативную реализацию
/// через таблицу сопряжённости (contingency table) с асимптотикой O(n · k)
static function Metrics.AdjustedRandIndex(yTrue, yPred: Vector): real; static function Metrics.AdjustedRandIndex(yTrue, yPred: Vector): real;
begin begin
if yTrue.Length <> yPred.Length then if yTrue.Length <> yPred.Length then

View file

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

View file

@ -35,6 +35,8 @@ type
function Transform(df: DataFrame): DataFrame; function Transform(df: DataFrame): DataFrame;
/// Выполняет Fit и Transform последовательно /// Выполняет Fit и Transform последовательно
function FitTransform(df: DataFrame): DataFrame; function FitTransform(df: DataFrame): DataFrame;
function Clone: IPreprocessor;
end; end;
/// Кодирует строковый категориальный столбец в целочисленные индексы (0,1,2,...). /// Кодирует строковый категориальный столбец в целочисленные индексы (0,1,2,...).
@ -62,6 +64,8 @@ type
function ToString: string; override; function ToString: string; override;
property ColumnName: string read col; property ColumnName: string read col;
function Clone: IPreprocessor;
end; end;
/// Кодирует строковый категориальный столбец в набор бинарных (one-hot) столбцов /// Кодирует строковый категориальный столбец в набор бинарных (one-hot) столбцов
@ -89,6 +93,8 @@ type
function ToString: string; override; function ToString: string; override;
property ColumnName: string read col; property ColumnName: string read col;
function Clone: IPreprocessor;
end; end;
ImputeStrategy = (isMean, isConstant, isMedian); ImputeStrategy = (isMean, isConstant, isMedian);
@ -123,6 +129,8 @@ type
function ToString: string; override; function ToString: string; override;
property Columns: array of string read cols; property Columns: array of string read cols;
function Clone: IPreprocessor;
end; end;
@ -194,6 +202,9 @@ const
'Массив констант не задан или имеет неверный размер!!Constants array is null or has invalid length'; 'Массив констант не задан или имеет неверный размер!!Constants array is null or has invalid length';
ER_IMPUTER_STRATEGY_NOT_SUPPORTED = ER_IMPUTER_STRATEGY_NOT_SUPPORTED =
'Стратегия импутации {0} не поддерживается!!Imputation strategy {0} is not supported'; 'Стратегия импутации {0} не поддерживается!!Imputation strategy {0} is not supported';
ER_UNSUPPORTED_IMPUTE_STRATEGY =
'Неподдерживаемая стратегия заполнения: {0}!!Unsupported impute strategy: {0}';
//----------------------------- //-----------------------------
// LabelEncoder // LabelEncoder
@ -287,7 +298,7 @@ begin
foreach var src in df.GetColumns do foreach var src in df.GetColumns do
if src.Info.Name <> col then if src.Info.Name <> col then
res.AddColumnView(src) res.AddColumnAlias(src)
else else
res.AddIntColumn(col, data, valid); res.AddIntColumn(col, data, valid);
@ -305,6 +316,10 @@ begin
Result := 'LabelEncoder(' + col + ')'; Result := 'LabelEncoder(' + col + ')';
end; end;
function LabelEncoder.Clone: IPreprocessor;
begin
Result := new LabelEncoder(col);
end;
//----------------------------- //-----------------------------
// OneHotEncoder // OneHotEncoder
//----------------------------- //-----------------------------
@ -414,6 +429,11 @@ begin
Result := 'OneHotEncoder(column=' + col + ')'; Result := 'OneHotEncoder(column=' + col + ')';
end; end;
function OneHotEncoder.Clone: IPreprocessor;
begin
Result := new OneHotEncoder(col);
end;
//----------------------------- //-----------------------------
// Imputer // Imputer
//----------------------------- //-----------------------------
@ -546,8 +566,10 @@ begin
for var i := 0 to cols.Length - 1 do for var i := 0 to cols.Length - 1 do
begin begin
var name := cols[i]; 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 if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then
Error(ER_IMPUTER_COLUMN_NOT_NUMERIC, name); Error(ER_IMPUTER_COLUMN_NOT_NUMERIC, name);
@ -571,12 +593,19 @@ begin
if ct = ColumnType.ctInt then if ct = ColumnType.ctInt then
begin begin
var k: integer; var k: integer;
try
k := integer(v); if v is integer then
except k := integer(v)
on e: Exception do 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); Error(ER_IMPUTER_CONSTANT_TYPE_MISMATCH, name);
end; k := ir;
end
else
Error(ER_IMPUTER_CONSTANT_TYPE_MISMATCH, name);
res := res.ReplaceColumnInt( res := res.ReplaceColumnInt(
name, name,
@ -599,7 +628,7 @@ begin
); );
end; end;
end; end;
isMedian: isMedian:
begin begin
var m := medians[i]; var m := medians[i];
@ -607,7 +636,7 @@ begin
name, name,
c -> (if c.IsValid(idx) then c.Float(idx) else m) c -> (if c.IsValid(idx) then c.Float(idx) else m)
); );
end; end;
else else
Error(ER_IMPUTER_STRATEGY_NOT_SUPPORTED, strategy); Error(ER_IMPUTER_STRATEGY_NOT_SUPPORTED, strategy);
@ -651,4 +680,24 @@ begin
end; end;
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. end.

View file

@ -108,7 +108,9 @@ const
'Некорректное значение параметра {0}!!Invalid value for parameter {0}'; 'Некорректное значение параметра {0}!!Invalid value for parameter {0}';
ER_DATASET_TOO_SMALL = ER_DATASET_TOO_SMALL =
'Для {0} требуется как минимум 2 объекта!!' + 'Для {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 // Validation
@ -319,7 +321,7 @@ begin
if cnt < k then if cnt < k then
ArgumentError( ArgumentError(
'StratifiedKFold: класс %d содержит %d объектов, что меньше числа фолдов (%d)', ER_STRATIFIED_CLASS_TOO_SMALL,
cls, cnt, k cls, cnt, k
); );
end; end;