pascalabcnet/bin/Lib/PreprocessorABC.pas
Mikhalkovich Stanislav 828484f15a Новые ML примеры и тесты - в основном в DataFrame
EncodeLabels - убрана проверка категориальности, добавлен тип Bool
В DataFrame добавлен тип DateTime
Добавлен тип DataValue позволяющий писать df.Filter(r -> r['Col'] > 5)
2026-06-19 22:06:08 +03:00

1593 lines
49 KiB
ObjectPascal
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) Ivan Bondarev, Stanislav Mikhalkovich (for details please see \doc\copyright.txt)
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
// PreprocessorABC v.1.0
{
DataFrameABC
PreprocessorABC
├─ IPreprocessor
├─ Scalers / Encoders / Imputer
└─ Pipeline
MLCore
}
/// PreprocessorABC — модуль подготовки табличных данных для анализа данных и машинного обучения.
/// Содержит типовые преобразования признаков с семантикой Fit / Transform.
/// Работает совместно с DataFrameABC и не содержит моделей машинного обучения.
unit PreprocessorABC;
interface
uses DataFrameABC;
uses DataFrameABCCore;
uses System;
uses MLCoreABC;
type
/// Базовый интерфейс шагов подготовки данных.
/// Определяет семантику операций Fit и Transform
IPreprocessor = interface(IDataStep)
/// Анализирует DataFrame и и сохраняет параметры шага
function Fit(df: DataFrame): IPreprocessor;
/// Применяет сохранённые параметры к DataFrame.
/// Возвращает новый DataFrame
function Transform(df: DataFrame): DataFrame;
/// Выполняет Fit и Transform последовательно
function FitTransform(df: DataFrame): DataFrame;
/// Копирует только конфигурацию препроцессора (без обученного состояния).
/// Используется для создания независимых экземпляров препроцессора.
function Clone: IPreprocessor;
end;
/// Кодирует строковый категориальный столбец в целочисленные индексы (0,1,2,...).
/// Соответствие значений и индексов фиксируется при вызове Fit
/// в порядке первого появления категорий.
/// Пропущенные значения (NA) игнорируются при обучении
/// и сохраняются как пропуски при преобразовании.
/// Работает только со строковыми столбцами и предназначен для признаков.
/// Не должен применяться к целевому столбцу (target)
OrdinalEncoder = class(IPreprocessor, IColumnBoundStep)
private
col: string;
mapping: Dictionary<string, integer>;
fitted: boolean;
public
/// Создаёт OrdinalEncoder для указанного столбца
constructor Create(column: string);
/// Определяет множество категорий столбца и сохраняет их числовое кодирование.
/// df — таблица данных.
/// Сохраняет отображение категорий в числовые коды для последующего применения.
///
/// Примечание:
/// • отображение категорий НЕ копируется методом Clone
function Fit(df: DataFrame): IPreprocessor;
/// Заменяет категории их числовыми кодами.
/// Неизвестные категории кодируются значением -1.
/// Пропущенные значения сохраняются как пропуски.
/// Возвращает новый DataFrame.
function Transform(df: DataFrame): DataFrame;
/// Выполняет Fit и Transform последовательно
function FitTransform(df: DataFrame): DataFrame;
function ToString: string; override;
property ColumnName: string read col;
/// Копирует только конфигурацию препроцессора (без обученного состояния).
/// Используется для создания независимых экземпляров препроцессора.
function Clone: IPreprocessor;
end;
/// Кодирует строковый категориальный столбец в набор бинарных (one-hot) столбцов
/// Категории фиксируются при Fit
/// Неизвестные категории кодируются нулями
/// Пропущенные значения (NA) кодируются нулями
OneHotEncoder = class(IPreprocessor, IColumnBoundStep, IColumnExpander)
private
col: string;
categories: array of string;
indexByValue: Dictionary<string, integer>;
fitted: boolean;
public
/// Создаёт OneHotEncoder для указанного столбца
constructor Create(column: string);
/// Определяет множество категорий столбца.
/// df — таблица данных.
/// Сохраняет категории для последующего преобразования в one-hot представление.
///
/// Примечание:
/// • категории НЕ копируются методом Clone
function Fit(df: DataFrame): IPreprocessor;
/// Заменяет столбец набором бинарных столбцов
/// Возвращает новый DataFrame
function Transform(df: DataFrame): DataFrame;
/// Выполняет Fit и Transform последовательно
function FitTransform(df: DataFrame): DataFrame;
function ToString: string; override;
property ColumnName: string read col;
function GetExpandedColumns(sourceColumn: string): array of string;
/// Копирует только конфигурацию препроцессора (без обученного состояния).
/// Используется для создания независимых экземпляров препроцессора.
function Clone: IPreprocessor;
end;
DateTimeUnit = (dtuDays, dtuHours, dtuMinutes, dtuSeconds);
/// Разворачивает DateTime-столбец в набор компонентных признаков.
/// По умолчанию создаёт признаки year, month, day.
/// Исходный DateTime-столбец заменяется новыми столбцами.
DateTimeComponentsEncoder = class(IPreprocessor, IColumnBoundStep, IColumnExpander)
private
col: string;
parts: array of DatePartKind;
expandedColumns: array of string;
fitted: boolean;
function DefaultParts: array of DatePartKind;
class function PartSuffix(part: DatePartKind): string;
public
/// Создаёт encoder со стандартным набором компонент: year, month, day.
constructor Create(column: string);
/// Создаёт encoder с явным списком компонент.
constructor Create(column: string; dateParts: array of DatePartKind);
function Fit(df: DataFrame): IPreprocessor;
function Transform(df: DataFrame): DataFrame;
function FitTransform(df: DataFrame): DataFrame;
function ToString: string; override;
property ColumnName: string read col;
function GetExpandedColumns(sourceColumn: string): array of string;
function Clone: IPreprocessor;
end;
/// Кодирует DateTime-столбец в один вещественный столбец,
/// содержащий расстояние от минимального значения столбца в выбранных единицах.
/// На Fit запоминается fOrigin = Min(column), на Transform вычисляется
/// (dt - fOrigin).TotalDays / TotalHours / TotalMinutes / TotalSeconds.
/// Исходный столбец заменяется вещественным столбцом с новым именем.
DateTimeOrdinalEncoder = class(IPreprocessor, IColumnBoundStep)
private
col: string;
newCol: string;
unit_: DateTimeUnit;
fOrigin: System.DateTime;
fitted: boolean;
class function DefaultColumnName(column: string; unit_: DateTimeUnit): string;
public
constructor Create(column: string);
constructor Create(column, newColumn: string);
constructor Create(column, newColumn: string; unit_: DateTimeUnit);
function Fit(df: DataFrame): IPreprocessor;
function Transform(df: DataFrame): DataFrame;
function FitTransform(df: DataFrame): DataFrame;
function ToString: string; override;
property ColumnName: string read col;
function Clone: IPreprocessor;
end;
/// Кодирует циклическую компоненту DateTime через пару sin/cos.
/// Например, для dpHour создаёт created_at_cyc_hour_sin и created_at_cyc_hour_cos.
/// Исходный DateTime-столбец заменяется двумя вещественными столбцами.
DateTimeCyclicEncoder = class(IPreprocessor, IColumnBoundStep, IColumnExpander)
private
col: string;
part: DatePartKind;
sinCol: string;
cosCol: string;
fitted: boolean;
class function PartSuffix(part: DatePartKind): string;
class function IsSupportedPart(part: DatePartKind): boolean;
class function PartPeriod(part: DatePartKind): integer;
class function PartValue(dt: System.DateTime; part: DatePartKind): real;
public
constructor Create(column: string; part: DatePartKind);
function Fit(df: DataFrame): IPreprocessor;
function Transform(df: DataFrame): DataFrame;
function FitTransform(df: DataFrame): DataFrame;
function ToString: string; override;
property ColumnName: string read col;
function GetExpandedColumns(sourceColumn: string): array of string;
function Clone: IPreprocessor;
end;
ImputeStrategy = (isMean, isConstant, isMedian);
/// Заполняет пропущенные значения (NA) в столбцах DataFrame
/// Стратегии isMean и isMedian работают только с числовыми столбцами
/// Стратегия isConstant работает с любыми поддерживаемыми типами
Imputer = class(IPreprocessor, IColumnsBoundStep)
private
cols: array of string;
strategy: ImputeStrategy;
constants: array of object;
means: array of real;
medians: array of real;
fitted: boolean;
function BuildImputedColumn(df: DataFrame; idx, imputerIndex: integer): Column;
public
/// Создаёт Imputer с заполнением средним значением
constructor Create(columns: array of string);
/// Создаёт Imputer с заданной стратегией заполнения
constructor Create(strategy: ImputeStrategy; columns: array of string);
/// Создаёт Imputer с константной стратегией заполнения
constructor Create(value: object; columns: array of string);
/// Вычисляет значения для заполнения пропусков.
/// df — таблица данных.
/// В зависимости от стратегии вычисляет параметры заполнения
/// (например, среднее, медиану или константу) для каждого столбца.
///
/// Примечание:
/// • вычисленные параметры НЕ копируются методом Clone
function Fit(df: DataFrame): IPreprocessor;
/// Заполняет пропущенные значения в DataFrame
/// Возвращает новый DataFrame
function Transform(df: DataFrame): DataFrame;
/// Выполняет Fit и Transform последовательно
function FitTransform(df: DataFrame): DataFrame;
function ToString: string; override;
property Columns: array of string read cols;
/// Копирует только конфигурацию препроцессора (без обученного состояния).
/// Используется для создания независимых экземпляров препроцессора.
function Clone: IPreprocessor;
end;
implementation
uses MLExceptions;
const
ER_ORDINALENCODER_NO_COLUMN =
'OrdinalEncoder: столбец не указан!!OrdinalEncoder: column not specified';
ER_ORDINALENCODER_NOT_STRING =
'OrdinalEncoder: столбец "{0}" не является строковым!!' +
'OrdinalEncoder: column "{0}" is not string';
ER_ORDINALENCODER_UNSEEN_CATEGORY =
'OrdinalEncoder: неизвестная категория "{0}"!!' +
'OrdinalEncoder: unseen category "{0}"';
ER_ONEHOT_NO_COLUMN =
'OneHotEncoder: столбец не указан!!OneHotEncoder: column not specified';
ER_ONEHOT_NOT_STRING =
'OneHotEncoder: столбец "{0}" не является строковым или не содержит допустимых значений!!' +
'OneHotEncoder: column "{0}" is not string or has no valid values';
ER_ONEHOT_UNSEEN_CATEGORY =
'OneHotEncoder: неизвестная категория "{0}"!!' +
'OneHotEncoder: unseen category "{0}"';
ER_IMPUTER_INVALID_STRATEGY_MEAN =
'Imputer: данный конструктор предназначен для стратегии isMean!!' +
'Imputer: this constructor is for isMean';
ER_IMPUTER_INVALID_STRATEGY_CONSTANT =
'Imputer: данный конструктор предназначен для стратегии isConstant!!' +
'Imputer: this constructor is for isConstant';
ER_IMPUTER_NO_COLUMNS =
'Imputer: столбцы не указаны!!Imputer: columns not specified';
ER_IMPUTER_COLUMN_NOT_NUMERIC =
'Imputer: столбец "{0}" не является числовым!!' +
'Imputer: column "{0}" is not numeric';
ER_IMPUTER_NO_VALID_VALUES =
'Imputer(mean): столбец "{0}" не содержит допустимых значений!!' +
'Imputer(mean): column "{0}" has no valid values';
ER_IMPUTER_CONSTANT_VALUE_NULL =
'Imputer(constant): значение nil для столбца "{0}"!!' +
'Imputer(constant): value is nil for column "{0}"';
ER_IMPUTER_CONSTANT_TYPE_MISMATCH =
'Imputer(constant): несоответствие типа значения для столбца "{0}"!!' +
'Imputer(constant): value type mismatch for column "{0}"';
ER_ONEHOT_EMPTY_COLUMN =
'Столбец "{0}" не содержит категориальных значений!!Column "{0}" contains no categorical values';
ER_IMPUTER_CONSTANTS_INVALID =
'Массив констант не задан или имеет неверный размер!!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}';
ER_UNSUPPORTED_COLUMN_TYPE =
'Неподдерживаемый тип столбца!!Unsupported column type';
ER_ONEHOT_NAME_EQUALS_SOURCE =
'Сгенерированная колонка совпадает с исходной: {0}!!Generated column equals source column: {0}';
ER_ONEHOT_COLUMN_COLLISION =
'Конфликт имён колонок: {0}!!Column name collision: {0}';
ER_ONEHOT_DUPLICATE_COLUMN =
'Дублирующаяся сгенерированная колонка: {0}!!Duplicate generated column: {0}';
ER_DTENC_NO_COLUMN =
'DateTimeComponentsEncoder: столбец не указан!!DateTimeComponentsEncoder: column not specified';
ER_DTENC_NOT_DATETIME =
'DateTimeComponentsEncoder: столбец "{0}" не является DateTime!!' +
'DateTimeComponentsEncoder: column "{0}" is not DateTime';
ER_DTENC_EMPTY_PARTS =
'DateTimeComponentsEncoder: список компонент пуст!!DateTimeComponentsEncoder: date parts list is empty';
ER_DTENC_DUPLICATE_PART =
'DateTimeComponentsEncoder: компонент даты повторяется: {0}!!DateTimeComponentsEncoder: duplicate date part: {0}';
ER_DTENC_NAME_EQUALS_SOURCE =
'DateTimeComponentsEncoder: сгенерированная колонка совпадает с исходной: {0}!!DateTimeComponentsEncoder: generated column equals source column: {0}';
ER_DTENC_COLUMN_COLLISION =
'DateTimeComponentsEncoder: конфликт имён колонок: {0}!!DateTimeComponentsEncoder: column name collision: {0}';
ER_DTENC_DUPLICATE_COLUMN =
'DateTimeComponentsEncoder: дублирующаяся сгенерированная колонка: {0}!!DateTimeComponentsEncoder: duplicate generated column: {0}';
ER_DTORD_NO_COLUMN =
'DateTimeOrdinalEncoder: столбец не указан!!DateTimeOrdinalEncoder: column not specified';
ER_DTORD_NO_NEW_COLUMN =
'DateTimeOrdinalEncoder: имя новой колонки не указано!!DateTimeOrdinalEncoder: new column name not specified';
ER_DTORD_NOT_DATETIME =
'DateTimeOrdinalEncoder: столбец "{0}" не является DateTime!!' +
'DateTimeOrdinalEncoder: column "{0}" is not DateTime';
ER_DTORD_NO_VALID_VALUES =
'DateTimeOrdinalEncoder: столбец "{0}" не содержит допустимых значений!!' +
'DateTimeOrdinalEncoder: column "{0}" has no valid values';
ER_DTORD_COLUMN_COLLISION =
'DateTimeOrdinalEncoder: конфликт имён колонок: {0}!!DateTimeOrdinalEncoder: column name collision: {0}';
ER_DTCYC_NO_COLUMN =
'DateTimeCyclicEncoder: столбец не указан!!DateTimeCyclicEncoder: column not specified';
ER_DTCYC_NOT_DATETIME =
'DateTimeCyclicEncoder: столбец "{0}" не является DateTime!!' +
'DateTimeCyclicEncoder: column "{0}" is not DateTime';
ER_DTCYC_UNSUPPORTED_PART =
'DateTimeCyclicEncoder: часть даты {0} не поддерживается для циклического кодирования!!' +
'DateTimeCyclicEncoder: date part {0} is not supported for cyclic encoding';
ER_DTCYC_COLUMN_COLLISION =
'DateTimeCyclicEncoder: конфликт имён колонок: {0}!!DateTimeCyclicEncoder: column name collision: {0}';
ER_DTCYC_DUPLICATE_COLUMN =
'DateTimeCyclicEncoder: дублирующаяся сгенерированная колонка: {0}!!DateTimeCyclicEncoder: duplicate generated column: {0}';
//-----------------------------
// OrdinalEncoder
//-----------------------------
constructor OrdinalEncoder.Create(column: string);
begin
if column = '' then
ArgumentError(ER_ORDINALENCODER_NO_COLUMN);
col := column;
fitted := false;
end;
function OrdinalEncoder.Fit(df: DataFrame): IPreprocessor;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if not df.HasColumn(col) then
ArgumentError(ER_COLUMN_NOT_FOUND, col);
var idx := df.Schema.IndexOf(col);
if df.Schema.ColumnTypeAt(idx) <> ColumnType.ctStr then
Error(ER_ORDINALENCODER_NOT_STRING, col);
mapping := new Dictionary<string, integer>;
var cur := df.GetCursor;
var nextId := 0;
while cur.MoveNext do
begin
if not cur.IsValid(idx) then continue;
var s := cur.Str(idx);
if not mapping.ContainsKey(s) then
begin
mapping[s] := nextId;
nextId += 1;
end;
end;
fitted := true;
Result := Self;
end;
function OrdinalEncoder.Transform(df: DataFrame): DataFrame;
begin
if not fitted then
NotFittedError(ER_FIT_NOT_CALLED);
var idx := df.Schema.IndexOf(col);
var n := df.RowCount;
var data := new integer[n];
var valid := new boolean[n];
var cur := df.GetCursor;
var row := 0;
while cur.MoveNext do
begin
if not cur.IsValid(idx) then
begin
valid[row] := False;
data[row] := 0;
end
else
begin
var s := cur.Str(idx);
if s not in mapping then
data[row] := -1
else
data[row] := mapping[s];
valid[row] := True;
end;
row += 1;
end;
var res := new DataFrame;
foreach var src in df.GetColumns do
if src.Info.Name <> col then
res.AddColumnAlias(src)
else
res.AddIntColumn(col, data, valid);
var catCols := new List<string>;
foreach var name in df.Schema.ColumnNames do
if df.IsCategorical(name) and (name <> col) then
catCols.Add(name);
catCols.Add(col); // encoded колонка тоже categorical
Result := res.SetCategorical(catCols.ToArray);
end;
function OrdinalEncoder.FitTransform(df: DataFrame): DataFrame;
begin
Fit(df);
Result := Transform(df);
end;
function OrdinalEncoder.ToString: string;
begin
Result := 'OrdinalEncoder(' + col + ')';
end;
function OrdinalEncoder.Clone: IPreprocessor;
begin
Result := new OrdinalEncoder(col);
end;
procedure AppendAllColumnsExcept(
res, src: DataFrame;
skipIndex: integer;
names: List<string>;
types: List<ColumnType>;
cats: List<boolean>
);
begin
for var i := 0 to src.ColumnCount - 1 do
if i <> skipIndex then
begin
res.AddColumnAlias(src.GetColumn(i));
names.Add(src.Schema.NameAt(i));
types.Add(src.Schema.ColumnTypeAt(i));
cats.Add(src.Schema.IsCategoricalAt(i));
end;
end;
//-----------------------------
// OneHotEncoder
//-----------------------------
constructor OneHotEncoder.Create(column: string);
begin
if column = '' then
ArgumentError(ER_ONEHOT_NO_COLUMN);
col := column;
fitted := false;
end;
function OneHotEncoder.Fit(df: DataFrame): IPreprocessor;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
var idx := df.Schema.IndexOf(col);
if idx < 0 then
Error(ER_COLUMN_NOT_FOUND, col);
if df.Schema.ColumnTypeAt(idx) <> ColumnType.ctStr then
Error(ER_ONEHOT_NOT_STRING, col);
indexByValue := new Dictionary<string, integer>;
var values := new List<string>;
var cur := df.GetCursor;
while cur.MoveNext do
begin
if not cur.IsValid(idx) then continue;
var s := cur.Str(idx);
var dummy: integer;
if not indexByValue.TryGetValue(s, dummy) then
begin
indexByValue[s] := values.Count;
values.Add(s);
end;
end;
if values.Count = 0 then
Error(ER_ONEHOT_EMPTY_COLUMN, col);
categories := values.ToArray;
// --- проверка коллизий имён колонок
var used := new HashSet<string>;
for var i := 0 to categories.Length - 1 do
begin
var newName := col + '_' + categories[i];
// 1. совпадение с исходным именем
if newName = col then
Error(ER_ONEHOT_NAME_EQUALS_SOURCE, newName);
// 2. коллизия с существующими колонками DataFrame
if df.HasColumn(newName) then
Error(ER_ONEHOT_COLUMN_COLLISION, newName);
// 3. дубликаты среди сгенерированных колонок
if used.Contains(newName) then
Error(ER_ONEHOT_DUPLICATE_COLUMN, newName);
used.Add(newName);
end;
fitted := true;
Result := Self;
end;
function OneHotEncoder.Transform(df: DataFrame): DataFrame;
begin
if not fitted then
NotFittedError(ER_FIT_NOT_CALLED);
var srcIdx := df.Schema.IndexOf(col);
if srcIdx < 0 then
ArgumentError(ER_COLUMN_NOT_FOUND, col);
if df.Schema.ColumnTypeAt(srcIdx) <> ColumnType.ctStr then
Error(ER_ONEHOT_NOT_STRING, col);
var rowCount := df.RowCount;
var catCount := categories.Length;
var res := new DataFrame;
var names := new List<string>;
var types := new List<ColumnType>;
var cats := new List<boolean>;
AppendAllColumnsExcept(res, df, srcIdx, names, types, cats);
var srcCol := StrColumn(df.GetColumn(srcIdx));
var srcData := srcCol.Data;
var srcValid := srcCol.IsValid;
var dataCols := new List<array of integer>;
var validCols := new List<array of boolean>;
for var j := 0 to catCount - 1 do
begin
dataCols.Add(new integer[rowCount]);
validCols.Add([True] * rowCount);
end;
for var row := 0 to rowCount - 1 do
begin
if (srcValid <> nil) and not srcValid[row] then
continue;
var s := srcData[row];
var idx: integer;
if indexByValue.TryGetValue(s, idx) then
dataCols[idx][row] := 1;
end;
for var j := 0 to catCount - 1 do
begin
var newName := col + '_' + categories[j];
res.AddIntColumn(newName, dataCols[j], validCols[j]);
names.Add(newName);
types.Add(ColumnType.ctInt);
cats.Add(False);
end;
res.SetSchema(new DataFrameSchema(
names.ToArray,
types.ToArray,
cats.ToArray
));
Result := res;
end;
function OneHotEncoder.FitTransform(df: DataFrame): DataFrame;
begin
Fit(df);
Result := Transform(df);
end;
function OneHotEncoder.GetExpandedColumns(sourceColumn: string): array of string;
begin
if not fitted then
NotFittedError(ER_FIT_NOT_CALLED);
if sourceColumn <> col then
exit(nil);
var res := new string[categories.Length];
for var i := 0 to categories.Length - 1 do
res[i] := col + '_' + categories[i];
Result := res;
end;
function OneHotEncoder.ToString: string;
begin
Result := 'OneHotEncoder(column=' + col + ')';
end;
function OneHotEncoder.Clone: IPreprocessor;
begin
Result := new OneHotEncoder(col);
end;
//-----------------------------
// DateTimeComponentsEncoder
//-----------------------------
function DateTimeComponentsEncoder.DefaultParts: array of DatePartKind;
begin
Result := [DatePartKind.dpYear, DatePartKind.dpMonth, DatePartKind.dpDay];
end;
class function DateTimeComponentsEncoder.PartSuffix(part: DatePartKind): string;
begin
case part of
dpYear: Result := 'year';
dpMonth: Result := 'month';
dpDay: Result := 'day';
dpHour: Result := 'hour';
dpMinute: Result := 'minute';
dpSecond: Result := 'second';
dpDayOfWeek: Result := 'dayofweek';
dpDate: Result := 'date';
end;
end;
constructor DateTimeComponentsEncoder.Create(column: string);
begin
if column = '' then
ArgumentError(ER_DTENC_NO_COLUMN);
col := column;
parts := DefaultParts;
fitted := false;
end;
constructor DateTimeComponentsEncoder.Create(column: string; dateParts: array of DatePartKind);
begin
if column = '' then
ArgumentError(ER_DTENC_NO_COLUMN);
if (dateParts = nil) or (dateParts.Length = 0) then
ArgumentError(ER_DTENC_EMPTY_PARTS);
col := column;
parts := Copy(dateParts);
fitted := false;
end;
function DateTimeComponentsEncoder.Fit(df: DataFrame): IPreprocessor;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
var idx := df.Schema.IndexOf(col);
if idx < 0 then
Error(ER_COLUMN_NOT_FOUND, col);
if df.Schema.ColumnTypeAt(idx) <> ColumnType.ctDateTime then
Error(ER_DTENC_NOT_DATETIME, col);
var usedParts := new HashSet<DatePartKind>;
for var i := 0 to parts.Length - 1 do
if usedParts.Contains(parts[i]) then
Error(ER_DTENC_DUPLICATE_PART, parts[i])
else
usedParts.Add(parts[i]);
expandedColumns := new string[parts.Length];
var usedNames := new HashSet<string>;
for var i := 0 to parts.Length - 1 do
begin
var newName := col + '_' + PartSuffix(parts[i]);
expandedColumns[i] := newName;
if newName = col then
Error(ER_DTENC_NAME_EQUALS_SOURCE, newName);
if df.HasColumn(newName) then
Error(ER_DTENC_COLUMN_COLLISION, newName);
if usedNames.Contains(newName) then
Error(ER_DTENC_DUPLICATE_COLUMN, newName);
usedNames.Add(newName);
end;
fitted := true;
Result := Self;
end;
function DateTimeComponentsEncoder.Transform(df: DataFrame): DataFrame;
begin
if not fitted then
NotFittedError(ER_FIT_NOT_CALLED);
var srcIdx := df.Schema.IndexOf(col);
if srcIdx < 0 then
ArgumentError(ER_COLUMN_NOT_FOUND, col);
if df.Schema.ColumnTypeAt(srcIdx) <> ColumnType.ctDateTime then
Error(ER_DTENC_NOT_DATETIME, col);
var rowCount := df.RowCount;
var srcCol := DateTimeColumn(df.GetColumn(srcIdx));
var srcData := srcCol.Data;
var srcValid := srcCol.IsValid;
var res := new DataFrame;
var names := new List<string>;
var types := new List<ColumnType>;
var cats := new List<boolean>;
AppendAllColumnsExcept(res, df, srcIdx, names, types, cats);
for var j := 0 to parts.Length - 1 do
begin
var part := parts[j];
var newName := expandedColumns[j];
if part = dpDate then
begin
var data := new System.DateTime[rowCount];
var valid := new boolean[rowCount];
for var row := 0 to rowCount - 1 do
if (srcValid <> nil) and not srcValid[row] then
begin
data[row] := default(System.DateTime);
valid[row] := False;
end
else
begin
data[row] := srcData[row].Date;
valid[row] := True;
end;
res.AddDateTimeColumn(newName, data, valid);
names.Add(newName);
types.Add(ColumnType.ctDateTime);
cats.Add(False);
end
else
begin
var data := new integer[rowCount];
var valid := new boolean[rowCount];
for var row := 0 to rowCount - 1 do
if (srcValid <> nil) and not srcValid[row] then
begin
data[row] := 0;
valid[row] := False;
end
else
begin
case part of
dpYear: data[row] := srcData[row].Year;
dpMonth: data[row] := srcData[row].Month;
dpDay: data[row] := srcData[row].Day;
dpHour: data[row] := srcData[row].Hour;
dpMinute: data[row] := srcData[row].Minute;
dpSecond: data[row] := srcData[row].Second;
dpDayOfWeek: data[row] := integer(srcData[row].DayOfWeek);
end;
valid[row] := True;
end;
res.AddIntColumn(newName, data, valid);
names.Add(newName);
types.Add(ColumnType.ctInt);
cats.Add(False);
end;
end;
res.SetSchema(new DataFrameSchema(
names.ToArray,
types.ToArray,
cats.ToArray
));
Result := res;
end;
function DateTimeComponentsEncoder.FitTransform(df: DataFrame): DataFrame;
begin
Fit(df);
Result := Transform(df);
end;
function DateTimeComponentsEncoder.ToString: string;
begin
Result := 'DateTimeComponentsEncoder(column=' + col + ')';
end;
function DateTimeComponentsEncoder.GetExpandedColumns(sourceColumn: string): array of string;
begin
if not fitted then
NotFittedError(ER_FIT_NOT_CALLED);
if sourceColumn <> col then
exit(nil);
Result := Copy(expandedColumns);
end;
function DateTimeComponentsEncoder.Clone: IPreprocessor;
begin
Result := new DateTimeComponentsEncoder(col, parts);
end;
//-----------------------------
// DateTimeOrdinalEncoder
//-----------------------------
class function DateTimeOrdinalEncoder.DefaultColumnName(column: string; unit_: DateTimeUnit): string;
begin
case unit_ of
dtuDays: Result := column + '_days';
dtuHours: Result := column + '_hours';
dtuMinutes: Result := column + '_minutes';
dtuSeconds: Result := column + '_seconds';
end;
end;
constructor DateTimeOrdinalEncoder.Create(column: string);
begin
Create(column, DefaultColumnName(column, DateTimeUnit.dtuDays), DateTimeUnit.dtuDays);
end;
constructor DateTimeOrdinalEncoder.Create(column, newColumn: string);
begin
Create(column, newColumn, DateTimeUnit.dtuDays);
end;
constructor DateTimeOrdinalEncoder.Create(column, newColumn: string; unit_: DateTimeUnit);
begin
if column = '' then
ArgumentError(ER_DTORD_NO_COLUMN);
if newColumn = '' then
ArgumentError(ER_DTORD_NO_NEW_COLUMN);
col := column;
newCol := newColumn;
self.unit_ := unit_;
fitted := false;
end;
function DateTimeOrdinalEncoder.Fit(df: DataFrame): IPreprocessor;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
var idx := df.Schema.IndexOf(col);
if idx < 0 then
Error(ER_COLUMN_NOT_FOUND, col);
if df.Schema.ColumnTypeAt(idx) <> ColumnType.ctDateTime then
Error(ER_DTORD_NOT_DATETIME, col);
if (newCol <> col) and df.HasColumn(newCol) then
Error(ER_DTORD_COLUMN_COLLISION, newCol);
var cur := df.GetCursor;
var hasValue := false;
var minValue := default(System.DateTime);
while cur.MoveNext do
if cur.IsValid(idx) then
begin
var dt := cur.DateTime(idx);
if not hasValue or (dt < minValue) then
begin
minValue := dt;
hasValue := true;
end;
end;
if not hasValue then
Error(ER_DTORD_NO_VALID_VALUES, col);
fOrigin := minValue;
fitted := true;
Result := Self;
end;
function DateTimeOrdinalEncoder.Transform(df: DataFrame): DataFrame;
begin
if not fitted then
NotFittedError(ER_FIT_NOT_CALLED);
var idx := df.Schema.IndexOf(col);
if idx < 0 then
ArgumentError(ER_COLUMN_NOT_FOUND, col);
if df.Schema.ColumnTypeAt(idx) <> ColumnType.ctDateTime then
Error(ER_DTORD_NOT_DATETIME, col);
var srcCol := DateTimeColumn(df.GetColumn(idx));
var rowCount := df.RowCount;
var data := new real[rowCount];
var valid := new boolean[rowCount];
for var row := 0 to rowCount - 1 do
if (srcCol.IsValid <> nil) and not srcCol.IsValid[row] then
begin
data[row] := 0.0;
valid[row] := False;
end
else
begin
var delta := srcCol.Data[row] - fOrigin;
case unit_ of
dtuDays: data[row] := delta.TotalDays;
dtuHours: data[row] := delta.TotalHours;
dtuMinutes: data[row] := delta.TotalMinutes;
dtuSeconds: data[row] := delta.TotalSeconds;
end;
valid[row] := True;
end;
var res := new DataFrame;
var names := new List<string>;
var types := new List<ColumnType>;
var cats := new List<boolean>;
for var i := 0 to df.ColumnCount - 1 do
if i <> idx then
begin
res.AddColumnAlias(df.GetColumn(i));
names.Add(df.Schema.NameAt(i));
types.Add(df.Schema.ColumnTypeAt(i));
cats.Add(df.Schema.IsCategoricalAt(i));
end
else
begin
res.AddFloatColumn(newCol, data, valid);
names.Add(newCol);
types.Add(ColumnType.ctFloat);
cats.Add(False);
end;
res.SetSchema(new DataFrameSchema(
names.ToArray,
types.ToArray,
cats.ToArray
));
Result := res;
end;
function DateTimeOrdinalEncoder.FitTransform(df: DataFrame): DataFrame;
begin
Fit(df);
Result := Transform(df);
end;
function DateTimeOrdinalEncoder.ToString: string;
begin
Result := 'DateTimeOrdinalEncoder(column=' + col + ', newColumn=' + newCol + ', unit=' + unit_.ToString + ')';
end;
function DateTimeOrdinalEncoder.Clone: IPreprocessor;
begin
Result := new DateTimeOrdinalEncoder(col, newCol, unit_);
end;
//-----------------------------
// DateTimeCyclicEncoder
//-----------------------------
class function DateTimeCyclicEncoder.PartSuffix(part: DatePartKind): string;
begin
case part of
dpMonth: Result := 'month';
dpDayOfWeek: Result := 'dayofweek';
dpHour: Result := 'hour';
dpMinute: Result := 'minute';
dpSecond: Result := 'second';
dpTimeOfDay: Result := 'timeofday';
end;
end;
class function DateTimeCyclicEncoder.IsSupportedPart(part: DatePartKind): boolean;
begin
Result := part in [dpMonth, dpDayOfWeek, dpHour, dpMinute, dpSecond, dpTimeOfDay];
end;
class function DateTimeCyclicEncoder.PartPeriod(part: DatePartKind): integer;
begin
case part of
dpMonth: Result := 12;
dpDayOfWeek: Result := 7;
dpHour: Result := 24;
dpMinute: Result := 60;
dpSecond: Result := 60;
dpTimeOfDay: Result := 86400;
else
Error(ER_DTCYC_UNSUPPORTED_PART, part);
end;
end;
class function DateTimeCyclicEncoder.PartValue(dt: System.DateTime; part: DatePartKind): real;
begin
case part of
dpMonth: Result := dt.Month - 1;
dpDayOfWeek: Result := integer(dt.DayOfWeek);
dpHour: Result := dt.Hour;
dpMinute: Result := dt.Minute;
dpSecond: Result := dt.Second;
dpTimeOfDay:
Result := dt.Hour * 3600.0 + dt.Minute * 60.0 + dt.Second + dt.Millisecond / 1000.0;
else
Error(ER_DTCYC_UNSUPPORTED_PART, part);
end;
end;
constructor DateTimeCyclicEncoder.Create(column: string; part: DatePartKind);
begin
if column = '' then
ArgumentError(ER_DTCYC_NO_COLUMN);
if not IsSupportedPart(part) then
Error(ER_DTCYC_UNSUPPORTED_PART, part);
col := column;
self.part := part;
var suffix := PartSuffix(part);
sinCol := col + '_cyc_' + suffix + '_sin';
cosCol := col + '_cyc_' + suffix + '_cos';
fitted := false;
end;
function DateTimeCyclicEncoder.Fit(df: DataFrame): IPreprocessor;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
var idx := df.Schema.IndexOf(col);
if idx < 0 then
Error(ER_COLUMN_NOT_FOUND, col);
if df.Schema.ColumnTypeAt(idx) <> ColumnType.ctDateTime then
Error(ER_DTCYC_NOT_DATETIME, col);
if df.HasColumn(sinCol) then
Error(ER_DTCYC_COLUMN_COLLISION, sinCol);
if df.HasColumn(cosCol) then
Error(ER_DTCYC_COLUMN_COLLISION, cosCol);
if sinCol = cosCol then
Error(ER_DTCYC_DUPLICATE_COLUMN, sinCol);
fitted := true;
Result := Self;
end;
function DateTimeCyclicEncoder.Transform(df: DataFrame): DataFrame;
begin
if not fitted then
NotFittedError(ER_FIT_NOT_CALLED);
var srcIdx := df.Schema.IndexOf(col);
if srcIdx < 0 then
ArgumentError(ER_COLUMN_NOT_FOUND, col);
if df.Schema.ColumnTypeAt(srcIdx) <> ColumnType.ctDateTime then
Error(ER_DTCYC_NOT_DATETIME, col);
var rowCount := df.RowCount;
var srcCol := DateTimeColumn(df.GetColumn(srcIdx));
var srcData := srcCol.Data;
var srcValid := srcCol.IsValid;
var period := PartPeriod(part);
var sinData := new real[rowCount];
var cosData := new real[rowCount];
var sinValid := new boolean[rowCount];
var cosValid := new boolean[rowCount];
for var row := 0 to rowCount - 1 do
if (srcValid <> nil) and not srcValid[row] then
begin
sinData[row] := 0.0;
cosData[row] := 0.0;
sinValid[row] := False;
cosValid[row] := False;
end
else
begin
var value := PartValue(srcData[row], part);
var angle := 2 * PI * value / period;
sinData[row] := Sin(angle);
cosData[row] := Cos(angle);
sinValid[row] := True;
cosValid[row] := True;
end;
var res := new DataFrame;
var names := new List<string>;
var types := new List<ColumnType>;
var cats := new List<boolean>;
AppendAllColumnsExcept(res, df, srcIdx, names, types, cats);
res.AddFloatColumn(sinCol, sinData, sinValid);
names.Add(sinCol);
types.Add(ColumnType.ctFloat);
cats.Add(False);
res.AddFloatColumn(cosCol, cosData, cosValid);
names.Add(cosCol);
types.Add(ColumnType.ctFloat);
cats.Add(False);
res.SetSchema(new DataFrameSchema(
names.ToArray,
types.ToArray,
cats.ToArray
));
Result := res;
end;
function DateTimeCyclicEncoder.FitTransform(df: DataFrame): DataFrame;
begin
Fit(df);
Result := Transform(df);
end;
function DateTimeCyclicEncoder.ToString: string;
begin
Result := 'DateTimeCyclicEncoder(column=' + col + ', part=' + part.ToString + ')';
end;
function DateTimeCyclicEncoder.GetExpandedColumns(sourceColumn: string): array of string;
begin
if not fitted then
NotFittedError(ER_FIT_NOT_CALLED);
if sourceColumn <> col then
exit(nil);
Result := [sinCol, cosCol];
end;
function DateTimeCyclicEncoder.Clone: IPreprocessor;
begin
Result := new DateTimeCyclicEncoder(col, part);
end;
//-----------------------------
// Imputer
//-----------------------------
constructor Imputer.Create(strategy: ImputeStrategy; columns: array of string);
begin
if (columns = nil) or (columns.Length = 0) then
ArgumentError(ER_IMPUTER_NO_COLUMNS);
self.strategy := strategy;
self.cols := columns;
self.constants := nil;
fitted := false;
end;
constructor Imputer.Create(columns: array of string);
begin
Create(ImputeStrategy.isMean, columns);
end;
constructor Imputer.Create(value: object; columns: array of string);
begin
if (columns = nil) or (columns.Length = 0) then
ArgumentError(ER_IMPUTER_NO_COLUMNS);
self.strategy := ImputeStrategy.isConstant;
self.cols := columns;
// одна и та же константа для всех столбцов
self.constants := new object[columns.Length];
for var i := 0 to columns.Length - 1 do
self.constants[i] := value;
fitted := false;
end;
function Imputer.Fit(df: DataFrame): IPreprocessor;
begin
case strategy of
isMean:
begin
SetLength(means, cols.Length);
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);
if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then
Error(ER_IMPUTER_COLUMN_NOT_NUMERIC, name);
var sum := 0.0;
var cnt := 0;
var cur := df.GetCursor;
while cur.MoveNext do
if cur.IsValid(idx) then
begin
sum += cur.Float(idx);
cnt += 1;
end;
if cnt = 0 then
Error(ER_IMPUTER_NO_VALID_VALUES, name);
means[i] := sum / cnt;
end;
end;
isConstant:
begin
// ничего делать не нужно
end;
isMedian:
begin
SetLength(medians, cols.Length);
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);
if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then
Error(ER_IMPUTER_COLUMN_NOT_NUMERIC, name);
// --- собираем значения
var values := new List<real>;
var cur := df.GetCursor;
while cur.MoveNext do
if cur.IsValid(idx) then
values.Add(cur.Float(idx));
if values.Count = 0 then
Error(ER_IMPUTER_NO_VALID_VALUES, name);
// --- сортируем
values.Sort;
// --- медиана
var n := values.Count;
if n mod 2 = 1 then
medians[i] := values[n div 2]
else
medians[i] := (values[n div 2 - 1] + values[n div 2]) / 2.0;
end;
end;
end;
fitted := true;
Result := Self;
end;
function Imputer.BuildImputedColumn(df: DataFrame; idx, imputerIndex: integer): Column;
begin
var name := cols[imputerIndex];
var ct := df.Schema.ColumnTypeAt(idx);
var capturedIdx := idx;
case strategy of
isMean:
begin
if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then
Error(ER_IMPUTER_COLUMN_NOT_NUMERIC, name);
var m := means[imputerIndex];
var rowCount := df.RowCount;
var data := new real[rowCount];
var valid := new boolean[rowCount];
var cur := df.GetCursor;
var row := 0;
while cur.MoveNext do
begin
data[row] := if cur.IsValid(capturedIdx) then cur.Float(capturedIdx) else m;
valid[row] := True;
row += 1;
end;
Result := new FloatColumn(name, data, valid);
end;
isConstant:
begin
var v := constants[imputerIndex];
if v = nil then
Error(ER_IMPUTER_CONSTANT_VALUE_NULL, name);
if ct = ColumnType.ctInt then
begin
var k: integer;
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);
var rowCount := df.RowCount;
var data := new integer[rowCount];
var valid := new boolean[rowCount];
var cur := df.GetCursor;
var row := 0;
while cur.MoveNext do
begin
data[row] := if cur.IsValid(capturedIdx) then cur.Int(capturedIdx) else k;
valid[row] := True;
row += 1;
end;
Result := new IntColumn(name, data, valid);
end
else if ct = ColumnType.ctFloat then
begin
var r: real;
try
r := real(v);
except
on e: Exception do
Error(ER_IMPUTER_CONSTANT_TYPE_MISMATCH, name);
end;
var rowCount := df.RowCount;
var data := new real[rowCount];
var valid := new boolean[rowCount];
var cur := df.GetCursor;
var row := 0;
while cur.MoveNext do
begin
data[row] := if cur.IsValid(capturedIdx) then cur.Float(capturedIdx) else r;
valid[row] := True;
row += 1;
end;
Result := new FloatColumn(name, data, valid);
end
else if ct = ColumnType.ctStr then
begin
var s: string;
try
s := string(v);
except
on e: Exception do
Error(ER_IMPUTER_CONSTANT_TYPE_MISMATCH, name);
end;
var rowCount := df.RowCount;
var data := new string[rowCount];
var valid := new boolean[rowCount];
var cur := df.GetCursor;
var row := 0;
while cur.MoveNext do
begin
data[row] := if cur.IsValid(capturedIdx) then cur.Str(capturedIdx) else s;
valid[row] := True;
row += 1;
end;
Result := new StrColumn(name, data, valid);
end
else if ct = ColumnType.ctBool then
begin
var b: boolean;
try
b := boolean(v);
except
on e: Exception do
Error(ER_IMPUTER_CONSTANT_TYPE_MISMATCH, name);
end;
var rowCount := df.RowCount;
var data := new boolean[rowCount];
var valid := new boolean[rowCount];
var cur := df.GetCursor;
var row := 0;
while cur.MoveNext do
begin
data[row] := if cur.IsValid(capturedIdx) then cur.Bool(capturedIdx) else b;
valid[row] := True;
row += 1;
end;
Result := new BoolColumn(name, data, valid);
end
else
Error(ER_UNSUPPORTED_COLUMN_TYPE, ct);
end;
isMedian:
begin
if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then
Error(ER_IMPUTER_COLUMN_NOT_NUMERIC, name);
var m := medians[imputerIndex];
var rowCount := df.RowCount;
var data := new real[rowCount];
var valid := new boolean[rowCount];
var cur := df.GetCursor;
var row := 0;
while cur.MoveNext do
begin
data[row] := if cur.IsValid(capturedIdx) then cur.Float(capturedIdx) else m;
valid[row] := True;
row += 1;
end;
Result := new FloatColumn(name, data, valid);
end;
else
Error(ER_IMPUTER_STRATEGY_NOT_SUPPORTED, strategy);
end;
end;
function Imputer.Transform(df: DataFrame): DataFrame;
begin
if not fitted then
NotFittedError(ER_FIT_NOT_CALLED);
// --- проверка constants
if (strategy = isConstant) and
((constants = nil) or (constants.Length <> cols.Length)) then
Error(ER_IMPUTER_CONSTANTS_INVALID);
var imputeCols := new HashSet<string>(cols);
var res := new DataFrame;
var names := new List<string>;
var types := new List<ColumnType>;
var cats := new List<boolean>;
for var i := 0 to df.ColumnCount - 1 do
begin
var name := df.Schema.NameAt(i);
var imputerIndex := cols.IndexOf(name);
if (name in imputeCols) and (imputerIndex >= 0) then
begin
var col := BuildImputedColumn(df, i, imputerIndex);
res.AddColumnAlias(col);
names.Add(name);
types.Add(col.Info.ColType);
cats.Add(df.Schema.IsCategoricalAt(i));
end
else
begin
res.AddColumnAlias(df.GetColumn(i));
names.Add(name);
types.Add(df.Schema.ColumnTypeAt(i));
cats.Add(df.Schema.IsCategoricalAt(i));
end;
end;
res.SetSchema(new DataFrameSchema(
names.ToArray,
types.ToArray,
cats.ToArray
));
Result := res;
end;
function Imputer.FitTransform(df: DataFrame): DataFrame;
begin
Fit(df);
Result := Transform(df);
end;
function Imputer.ToString: string;
begin
var colsStr := '[' + cols.JoinToString(', ') + ']';
case strategy of
isMean:
Result := 'Imputer(strategy=mean, columns=' + colsStr + ')';
isMedian:
Result := 'Imputer(strategy=median, columns=' + colsStr + ')';
isConstant:
begin
var valStr :=
if (constants <> nil) and (constants.Length > 0) and (constants[0] <> nil) then
constants[0].ToString
else
'null';
Result := 'Imputer(strategy=constant, value=' +
valStr + ', columns=' + colsStr + ')';
end;
else
Result := 'Imputer(strategy=unknown, columns=' + colsStr + ')';
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.