// 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; 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; 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; 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; 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; types: List; cats: List ); 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; var values := new List; 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; 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; var types := new List; var cats := new List; 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; var validCols := new List; 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; 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; 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; var types := new List; var cats := new List; 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; var types := new List; var cats := new List; 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; var types := new List; var cats := new List; 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; 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(cols); var res := new DataFrame; var names := new List; var types := new List; var cats := new List; 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.