diff --git a/Configuration/GlobalAssemblyInfo.cs b/Configuration/GlobalAssemblyInfo.cs index 64fe7363e..afc5a8f17 100644 --- a/Configuration/GlobalAssemblyInfo.cs +++ b/Configuration/GlobalAssemblyInfo.cs @@ -15,7 +15,7 @@ internal static class RevisionClass public const string Major = "3"; public const string Minor = "11"; public const string Build = "1"; - public const string Revision = "3812"; + public const string Revision = "3814"; public const string MainVersion = Major + "." + Minor; public const string FullVersion = Major + "." + Minor + "." + Build + "." + Revision; diff --git a/Configuration/Version.defs b/Configuration/Version.defs index f399f3dad..2ed31f4f6 100644 --- a/Configuration/Version.defs +++ b/Configuration/Version.defs @@ -1,4 +1,4 @@ %COREVERSION%=1 -%REVISION%=3812 +%REVISION%=3814 %MINOR%=11 %MAJOR%=3 diff --git a/InstallerSamples/MachineLearning/01_GettingStarted/01_FirstClassification_DataFrame.pas b/InstallerSamples/MachineLearning/01_GettingStarted/01_FirstClassification_DataFrame.pas new file mode 100644 index 000000000..c9d3780c1 --- /dev/null +++ b/InstallerSamples/MachineLearning/01_GettingStarted/01_FirstClassification_DataFrame.pas @@ -0,0 +1,38 @@ +// Первый пример классификации. +// +// В этом примере: +// 1) загружаем готовый датасет Iris; +// 2) выделяем признаки и целевую переменную; +// 3) делим данные на обучающую и тестовую выборки; +// 4) обучаем логистическую регрессию; +// 5) оцениваем точность модели. +uses MLABC; + +begin + // Загружаем учебный датасет Iris + var ds := Datasets.Iris; + + // Берём таблицу данных из датасета + var df := ds.Data; + + // Преобразуем выбранные признаки в числовую матрицу + var X := df.ToMatrix(ds.Features); + + // Кодируем названия классов в числа 0, 1, 2 + var y := df.EncodeLabels(ds.Target); + + // Делим данные на обучающую и тестовую выборки + var (Xtrain, Xtest, ytrain, ytest) := Validation.TrainTestSplit(X, y, 0.2, 1); + + // Создаём модель логистической регрессии + var model := new LogisticRegression; + + // Обучаем модель на обучающей выборке + model.Fit(Xtrain, ytrain); + + // Получаем предсказания для тестовой выборки + var pred := model.Predict(Xtest); + + // Считаем долю правильных ответов + Println('Точность:', Metrics.Accuracy(ytest, pred):0:3); +end. diff --git a/InstallerSamples/MachineLearning/01_GettingStarted/02_FirstRegression_DataFrame.pas b/InstallerSamples/MachineLearning/01_GettingStarted/02_FirstRegression_DataFrame.pas new file mode 100644 index 000000000..4827d3a60 --- /dev/null +++ b/InstallerSamples/MachineLearning/01_GettingStarted/02_FirstRegression_DataFrame.pas @@ -0,0 +1,46 @@ +// Первый пример регрессии. +// +// В этом примере: +// 1) загружаем готовый датасет с ценами на квартиры; +// 2) выбираем признаки и целевую переменную; +// 3) делим данные на обучающую и тестовую выборки; +// 4) обучаем линейную регрессию; +// 5) оцениваем качество модели по метрике R². +uses MLABC; + +begin + // Загружаем учебный датасет с ценами на квартиры в Москве + var ds := Datasets.MoscowHousing; + + // Берём таблицу данных из датасета + var df := ds.Data; + + // Выбираем числовые признаки, по которым будем предсказывать цену + var features := ['rooms', 'area', 'kitchen_area', 'floor', 'floors_total', 'metro_minutes']; + + // Целевая переменная - цена квартиры + var target := 'price'; + + // Преобразуем выбранные признаки в числовую матрицу + var X := df.ToMatrix(features); + + // Преобразуем целевую переменную в числовой вектор + var y := df.ToVector(target); + + // Делим данные на обучающую и тестовую выборки + var (Xtrain, Xtest, ytrain, ytest) := Validation.TrainTestSplit(X, y, 0.2, 42); + + // Создаём модель линейной регрессии + var model := new LinearRegression; + + // Обучаем модель на обучающей выборке + model.Fit(Xtrain, ytrain); + + // Получаем предсказания для тестовой выборки + var pred := model.Predict(Xtest); + + // Считаем, насколько хорошо модель объясняет данные. + // Для простой линейной модели R² около 0.6 и выше здесь уже неплохой результат, + // но более сложные модели могут работать точнее. + Println('R²:', Metrics.R2(ytest, pred):0:3); +end. diff --git a/InstallerSamples/MachineLearning/01_GettingStarted/03_FirstClustering_DataFrame.pas b/InstallerSamples/MachineLearning/01_GettingStarted/03_FirstClustering_DataFrame.pas new file mode 100644 index 000000000..a0f43ada6 --- /dev/null +++ b/InstallerSamples/MachineLearning/01_GettingStarted/03_FirstClustering_DataFrame.pas @@ -0,0 +1,84 @@ +// Первый пример кластеризации. +// +// В этом примере: +// 1) загружаем готовый датасет городов России; +// 2) выбираем признаки для кластеризации; +// 3) обучаем алгоритм KMeans; +// 4) получаем номер кластера для каждого города; +// 5) выводим несколько представителей каждого кластера. +// +// Важно: +// здесь кластеры отражают прежде всего географическую близость городов, +// потому что для кластеризации используются только широта и долгота. +// Население нужно только для выбора самых заметных представителей кластера. +// +// При числе кластеров k = 4 результат можно интерпретировать так: +// • один кластер обычно соответствует Европейской части России; +// • один - Уралу, Поволжью и соседним территориям; +// • один - Сибири; +// • один - Дальнему Востоку и северо-востоку. +uses MLABC; + +begin + // Загружаем учебный датасет с городами России + var ds := Datasets.RussianCities; + + // Берём таблицу данных из датасета + var df := ds.Data; + + // Для первого примера используем только координаты города + var features := ['lat', 'lon']; + + // Преобразуем выбранные признаки в числовую матрицу + var X := df.ToMatrix(features); + + // Создаём модель KMeans и просим разбить города на 4 группы. + // Для первого примера этого достаточно, чтобы увидеть общую структуру данных. + var model := new KMeans(4, seed := 42); + + // Обучаем модель на всех объектах + model.Fit(X); + + // Получаем номер кластера для каждого города + var labels := model.PredictLabels(X); + + // Получаем названия городов + var cities := df.GetStrColumn('city'); + + // Получаем численность населения городов + var populations := df.GetFloatColumn('population'); + + // Выводим, сколько кластеров нашёл алгоритм + Println($'Число кластеров: {model.ClustersCount}'); + + Println; + Println('Примеры городов в каждом кластере (в скобках - тыс. жителей):'); + + // Для каждого кластера выводим три самых крупных города + for var cluster := 0 to model.ClustersCount - 1 do + begin + Println; + Println($'Кластер {cluster + 1}:'); + + var clusterIndices := new List; + + for var i := 0 to labels.Length - 1 do + if labels[i] = cluster then + clusterIndices.Add(i); + + clusterIndices := clusterIndices + .OrderByDescending(i -> populations[i]) + .ToList; + + var shown := 0; + + foreach var i in clusterIndices do + begin + Println($' {cities[i]} ({populations[i]:F0})'); + shown += 1; + + if shown = 4 then + break; + end; + end; +end. diff --git a/InstallerSamples/MachineLearning/01_GettingStarted/04_FirstPipeline_DataFrame.pas b/InstallerSamples/MachineLearning/01_GettingStarted/04_FirstPipeline_DataFrame.pas new file mode 100644 index 000000000..9240365f7 --- /dev/null +++ b/InstallerSamples/MachineLearning/01_GettingStarted/04_FirstPipeline_DataFrame.pas @@ -0,0 +1,59 @@ +// Первый пример работы с pipeline. +// +// В этом примере: +// 1) загружаем готовый датасет Iris; +// 2) делим данные на обучающую и тестовую выборки; +// 3) собираем pipeline из масштабирования и модели; +// 4) обучаем pipeline; +// 5) оцениваем точность и смотрим вероятности классов. +// +// Pipeline удобен тем, что он сам выполняет все шаги по порядку: +// подготовку признаков, обучение модели и предсказание. +uses MLABC; + +begin + // Загружаем учебный датасет Iris + var ds := Datasets.Iris; + + // Берём таблицу данных из датасета + var df := ds.Data; + + // Делим данные на обучающую и тестовую выборки + var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 3); + + // Создаём pipeline: + // сначала масштабируем признаки, + // затем обучаем логистическую регрессию + var pipe := + DataPipeline.Build( + TaskKind.tkClassification, + ds.Target, + ds.Features, + new StandardScaler, + new LogisticRegression + ); + + // Обучаем весь pipeline на обучающей выборке + pipe.Fit(trainDf); + + // Получаем предсказанные классы для тестовой выборки + var pred := pipe.Predict(testDf); + + // Получаем правильные метки классов в кодировке pipeline + var y := pipe.GetEncodedLabels(testDf); + + // Считаем долю правильных ответов + Println($'Точность: {Metrics.Accuracy(y, pred):F3}'); + + // Получаем вероятности классов для объектов тестовой выборки + var proba := pipe.PredictProba(testDf); + + // Получаем названия классов в правильном порядке + var classes := pipe.GetClassLabels; + + Println($'Классы: {classes.JoinToString('', '')}'); + Println('Вероятности для первого объекта:'); + + for var j := 0 to classes.Length - 1 do + Println($'{classes[j]}: {proba[0, j]:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/02_DataFrame/df19.pas b/InstallerSamples/MachineLearning/02_DataFrame/df19.pas index 02de4714b..0bbcfdce2 100644 --- a/InstallerSamples/MachineLearning/02_DataFrame/df19.pas +++ b/InstallerSamples/MachineLearning/02_DataFrame/df19.pas @@ -12,5 +12,5 @@ begin Kat,21,NA '''); - df.PrintPreview(3); + df.Print; end. \ No newline at end of file diff --git a/InstallerSamples/MachineLearning/02_DataFrame/df20.pas b/InstallerSamples/MachineLearning/02_DataFrame/df20.pas index 90a188879..9e56f1453 100644 --- a/InstallerSamples/MachineLearning/02_DataFrame/df20.pas +++ b/InstallerSamples/MachineLearning/02_DataFrame/df20.pas @@ -17,5 +17,5 @@ begin .SortBy('age', descending := True) .GroupBy('age') .Mean('score') - .PrintPreview(3); + .Print; end. \ No newline at end of file diff --git a/InstallerSamples/MachineLearning/02_DataFrame/df21.pas b/InstallerSamples/MachineLearning/02_DataFrame/df21.pas index bbcc4547a..31c0669d8 100644 --- a/InstallerSamples/MachineLearning/02_DataFrame/df21.pas +++ b/InstallerSamples/MachineLearning/02_DataFrame/df21.pas @@ -14,5 +14,5 @@ begin df.Filter(r -> r.IsValid('score')) .SortBy(['age','score'],[True,True]) - .Println; + .Print; end. \ No newline at end of file diff --git a/InstallerSamples/MachineLearning/03_Preprocessing/01_StandardScaler.pas b/InstallerSamples/MachineLearning/03_Preprocessing/01_StandardScaler.pas new file mode 100644 index 000000000..9c07ec908 --- /dev/null +++ b/InstallerSamples/MachineLearning/03_Preprocessing/01_StandardScaler.pas @@ -0,0 +1,31 @@ +// StandardScaler приводит признаки к сопоставимому масштабу: +// после преобразования у каждого столбца среднее близко к 0, +// а стандартное отклонение - к 1. +uses MLABC; + +begin + var X := new Matrix(5, 2); + + // Первый признак имеет маленький масштаб и неравномерный шаг + X[0,0] := 1; X[1,0] := 2; X[2,0] := 3; X[3,0] := 10; X[4,0] := 20; + + // Второй признак имеет гораздо больший масштаб + X[0,1] := 100; X[1,1] := 120; X[2,1] := 150; X[3,1] := 300; X[4,1] := 500; + + Println('До масштабирования:'); + Println($'Средние: {X.ColumnMeans[0]:F3}, {X.ColumnMeans[1]:F3}'); + Println($'Стандартные отклонения: {X.ColumnStd(0):F3}, {X.ColumnStd(1):F3}'); + Println; + + var scaler := new StandardScaler; + scaler.Fit(X); + + var Xscaled := scaler.Transform(X); + + Println('После StandardScaler:'); + Println($'Средние: {Xscaled.ColumnMeans[0]:F3}, {Xscaled.ColumnMeans[1]:F3}'); + Println($'Стандартные отклонения: {Xscaled.ColumnStd(0):F3}, {Xscaled.ColumnStd(1):F3}'); + Println; + Println('Преобразованные значения:'); + Xscaled.Print; +end. diff --git a/InstallerSamples/MachineLearning/03_Preprocessing/02_MinMaxScaler.pas b/InstallerSamples/MachineLearning/03_Preprocessing/02_MinMaxScaler.pas new file mode 100644 index 000000000..7713a3971 --- /dev/null +++ b/InstallerSamples/MachineLearning/03_Preprocessing/02_MinMaxScaler.pas @@ -0,0 +1,32 @@ +// MinMaxScaler переводит значения каждого признака в диапазон [0, 1]. +// +// Это полезно, когда важно сохранить относительный порядок значений, +// но привести признаки к единому диапазону. +uses MLABC; + +begin + var X := new Matrix(5, 2); + + // Первый признак имеет маленький масштаб и неравномерный шаг + X[0,0] := 1; X[1,0] := 2; X[2,0] := 3; X[3,0] := 10; X[4,0] := 20; + + // Второй признак имеет гораздо больший масштаб + X[0,1] := 100; X[1,1] := 120; X[2,1] := 150; X[3,1] := 300; X[4,1] := 500; + + Println('До масштабирования:'); + Println($'Минимумы: {X.ColumnMin(0):F3}, {X.ColumnMin(1):F3}'); + Println($'Максимумы: {X.ColumnMax(0):F3}, {X.ColumnMax(1):F3}'); + Println; + + var scaler := new MinMaxScaler; + scaler.Fit(X); + + var Xscaled := scaler.Transform(X); + + Println('После MinMaxScaler:'); + Println($'Минимумы: {Xscaled.ColumnMin(0):F3}, {Xscaled.ColumnMin(1):F3}'); + Println($'Максимумы: {Xscaled.ColumnMax(0):F3}, {Xscaled.ColumnMax(1):F3}'); + Println; + Println('Преобразованные значения:'); + Xscaled.Print; +end. diff --git a/InstallerSamples/MachineLearning/03_Preprocessing/03_Imputer_LabelEncoder.pas b/InstallerSamples/MachineLearning/03_Preprocessing/03_Imputer_LabelEncoder.pas new file mode 100644 index 000000000..2f5fd8104 --- /dev/null +++ b/InstallerSamples/MachineLearning/03_Preprocessing/03_Imputer_LabelEncoder.pas @@ -0,0 +1,30 @@ +// Пример табличной предобработки: +// сначала заполняем пропуски в числовом столбце, +// затем кодируем категориальный столбец числами. +uses MLABC; + +begin + var df := DataFrame.FromCsvText(''' +city,population,region +Ростов-на-Дону,1142,Юг +Таганрог,NA,Юг +Воронеж,1058,Центр +Курск,452,Центр +Белгород,392,Центр +'''); + + Println('Исходные данные:'); + df.Print; + Println; + + // Заполняем пропуск в числовом столбце средним значением + var imputer := new Imputer(['population']); + df := imputer.FitTransform(df); + + // Кодируем названия регионов числами 0, 1, 2, ... + var encoder := new LabelEncoder('region'); + df := encoder.FitTransform(df); + + Println('После Imputer и LabelEncoder:'); + df.Print; +end. diff --git a/InstallerSamples/MachineLearning/03_Preprocessing/04_OneHotEncoder.pas b/InstallerSamples/MachineLearning/03_Preprocessing/04_OneHotEncoder.pas new file mode 100644 index 000000000..c4499e9f8 --- /dev/null +++ b/InstallerSamples/MachineLearning/03_Preprocessing/04_OneHotEncoder.pas @@ -0,0 +1,24 @@ +// OneHotEncoder заменяет категориальный столбец +// несколькими бинарными столбцами - по одному на каждую категорию. +uses MLABC; + +begin + var df := DataFrame.FromCsvText(''' +city,region +Ростов-на-Дону,Юг +Таганрог,Юг +Воронеж,Центр +Курск,Центр +Хабаровск,Дальний Восток +'''); + + Println('Исходные данные:'); + df.Print; + Println; + + var encoder := new OneHotEncoder('region'); + df := encoder.FitTransform(df); + + Println('После OneHotEncoder:'); + df.Print; +end. diff --git a/InstallerSamples/MachineLearning/03_Preprocessing/05_Normalizer.pas b/InstallerSamples/MachineLearning/03_Preprocessing/05_Normalizer.pas new file mode 100644 index 000000000..4a523a556 --- /dev/null +++ b/InstallerSamples/MachineLearning/03_Preprocessing/05_Normalizer.pas @@ -0,0 +1,22 @@ +// В этом примере показано, +// как Normalizer нормализует строки матрицы. + +uses MLABC; + +begin + var X := new Matrix(3, 2); + X[0, 0] := 3; X[0, 1] := 4; + X[1, 0] := 1; X[1, 1] := 2; + X[2, 0] := 5; X[2, 1] := 12; + + Println('До Normalizer:'); + X.Println; + Println; + + var norm := new Normalizer(NormType.L2); + norm.Fit(X); + var Xn := norm.Transform(X); + + Println('После Normalizer:'); + Xn.Println; +end. diff --git a/InstallerSamples/MachineLearning/03_Preprocessing/06_PCA.pas b/InstallerSamples/MachineLearning/03_Preprocessing/06_PCA.pas new file mode 100644 index 000000000..6494ead24 --- /dev/null +++ b/InstallerSamples/MachineLearning/03_Preprocessing/06_PCA.pas @@ -0,0 +1,24 @@ +// В этом примере PCA уменьшает число признаков +// с двух до одного. + +uses MLABC; + +begin + var X := new Matrix(5, 2); + X[0, 0] := 1; X[0, 1] := 2; + X[1, 0] := 2; X[1, 1] := 4; + X[2, 0] := 3; X[2, 1] := 6; + X[3, 0] := 4; X[3, 1] := 8; + X[4, 0] := 5; X[4, 1] := 10; + + Println('Размер до PCA: ', X.RowCount, 'x', X.ColCount); + + var pca := new PCATransformer(1); + pca.Fit(X); + var X1 := pca.Transform(X); + + Println('Размер после PCA: ', X1.RowCount, 'x', X1.ColCount); + Println; + Println('Преобразованные данные:'); + X1.Println; +end. diff --git a/InstallerSamples/MachineLearning/03_Preprocessing/07_VarianceThreshold.pas b/InstallerSamples/MachineLearning/03_Preprocessing/07_VarianceThreshold.pas new file mode 100644 index 000000000..fb71e815a --- /dev/null +++ b/InstallerSamples/MachineLearning/03_Preprocessing/07_VarianceThreshold.pas @@ -0,0 +1,23 @@ +// В этом примере VarianceThreshold удаляет признак, +// у которого нет разброса значений. + +uses MLABC; + +begin + var X := new Matrix(4, 3); + X[0, 0] := 1; X[0, 1] := 10; X[0, 2] := 5; + X[1, 0] := 2; X[1, 1] := 10; X[1, 2] := 6; + X[2, 0] := 3; X[2, 1] := 10; X[2, 2] := 7; + X[3, 0] := 4; X[3, 1] := 10; X[3, 2] := 8; + + Println('Размер до VarianceThreshold: ', X.RowCount, 'x', X.ColCount); + + var vt := new VarianceThreshold(0.001); + vt.Fit(X); + var X2 := vt.Transform(X); + + Println('Размер после VarianceThreshold: ', X2.RowCount, 'x', X2.ColCount); + Println; + Println('Преобразованные данные:'); + X2.Println; +end. diff --git a/InstallerSamples/MachineLearning/03_Preprocessing/08_SelectKBest.pas b/InstallerSamples/MachineLearning/03_Preprocessing/08_SelectKBest.pas new file mode 100644 index 000000000..6f9077521 --- /dev/null +++ b/InstallerSamples/MachineLearning/03_Preprocessing/08_SelectKBest.pas @@ -0,0 +1,55 @@ +// В этом примере SelectKBest оставляет +// только два самых полезных признака +// и показывает, как это влияет на качество модели. + +uses MLABC; + +begin + var (X, y) := Datasets.MakeClassification( + n := 200, + nFeatures := 6, + nInformative := 2, + nRedundant := 0, + noise := 0.1, + classSep := 1.2, + seed := 42 + ); + + var featureNames := ['f1', 'f2', 'f3', 'f4', 'f5', 'f6']; + + Println('Размер до SelectKBest: ', X.RowCount, 'x', X.ColCount); + + var skb := new SelectKBest(2, FeatureScore.Correlation); + skb.Fit(X, y); + var X2 := skb.Transform(X); + + Println('Размер после SelectKBest: ', X2.RowCount, 'x', X2.ColCount); + Println; + Println('Отобранные признаки:'); + + var selected := skb.SelectedFeatures; + for var i := 0 to selected.Length - 1 do + Println(' ', featureNames[selected[i]]); + + Println; + + var fullScore := Validation.StratifiedCrossValidate( + new LogisticRegression(learningRate := 0.05, epochs := 1000), + X, y, + 5, + ClassificationMetrics.Accuracy, + seed := 42 + ); + + var reducedScore := Validation.StratifiedCrossValidate( + new LogisticRegression(learningRate := 0.05, epochs := 1000), + X2, y, + 5, + ClassificationMetrics.Accuracy, + seed := 42 + ); + + Println('Сравнение качества LogisticRegression:'); + Println($' Все признаки: Accuracy = {fullScore:F3}'); + Println($' После SelectKBest: Accuracy = {reducedScore:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/03_Preprocessing/09_PreprocessingChain.pas b/InstallerSamples/MachineLearning/03_Preprocessing/09_PreprocessingChain.pas new file mode 100644 index 000000000..4cec2a39b --- /dev/null +++ b/InstallerSamples/MachineLearning/03_Preprocessing/09_PreprocessingChain.pas @@ -0,0 +1,30 @@ +// В этом примере показана ручная цепочка предобработки таблицы. +// +// Сначала заполняем пропуск в числовом столбце, +// затем превращаем категориальный столбец в набор бинарных признаков. + +uses MLABC; + +begin + var df := DataFrame.FromCsvText(''' +city,population,region +Ростов-на-Дону,1142,Юг +Таганрог,NA,Юг +Воронеж,1058,Центр +Курск,452,Центр +Хабаровск,616,Дальний Восток +'''); + + Println('Исходные данные:'); + df.Print; + Println; + + var imputer := new Imputer(['population']); + df := imputer.FitTransform(df); + + var encoder := new OneHotEncoder('region'); + df := encoder.FitTransform(df); + + Println('После цепочки Imputer -> OneHotEncoder:'); + df.Print; +end. diff --git a/InstallerSamples/MachineLearning/04_Models/02_ClassificationBasic.pas b/InstallerSamples/MachineLearning/04_Models/02_ClassificationBasic.pas new file mode 100644 index 000000000..0e9eb448a --- /dev/null +++ b/InstallerSamples/MachineLearning/04_Models/02_ClassificationBasic.pas @@ -0,0 +1,31 @@ +// Базовый пример логистической регрессии на датасете Iris. +// +// Iris - удобный учебный датасет для первого знакомства с классификацией: +// классы в нём разделяются достаточно хорошо, поэтому простая линейная модель +// уже даёт высокую точность. +uses MLABC; + +begin + var ds := Datasets.Iris; + var df := ds.Data; + + var X := df.ToMatrix(ds.Features); + var y := df.EncodeLabels(ds.Target); + + var (Xtrain, Xtest, ytrain, ytest) := + Validation.TrainTestSplit(X, y, testRatio := 0.2, seed := 1); + + var model := new LogisticRegression; + model.Fit(Xtrain, ytrain); + + var pred := model.Predict(Xtest); + var proba := model.PredictProba(Xtest); + + Println($'Точность на тестовой выборке: {Metrics.Accuracy(ytest, pred):F3}'); + Println; + Println('Вероятности для первого объекта:'); + + var classes := model.GetClasses; + for var j := 0 to classes.Length - 1 do + Println($'Класс {classes[j]:F0}: {proba[0, j]:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/04_Models/03_RegressionCorrelated.pas b/InstallerSamples/MachineLearning/04_Models/03_RegressionCorrelated.pas index fff125515..acf57d3a3 100644 --- a/InstallerSamples/MachineLearning/04_Models/03_RegressionCorrelated.pas +++ b/InstallerSamples/MachineLearning/04_Models/03_RegressionCorrelated.pas @@ -81,10 +81,4 @@ begin Println($'Linear MSE: {Metrics.MSE(yTest, yTestLR),0:F4}'); Println($'Ridge MSE: {Metrics.MSE(yTest, yTestRidge),0:F4}'); Println($'ElasticNet MSE: {Metrics.MSE(yTest, yTestEN),0:F4}'); - - {Println; - Println('Коэффициенты (первые 10):'); - lr.Coefficients.ToArray.Take(10).Println; - ridge.Coefficients.ToArray.Take(10).Println; - en.Coefficients.ToArray.Take(10).Println;} end. \ No newline at end of file diff --git a/InstallerSamples/MachineLearning/04_Models/11_KNN_WithScaling.pas b/InstallerSamples/MachineLearning/04_Models/11_KNN_WithScaling.pas new file mode 100644 index 000000000..567147c13 --- /dev/null +++ b/InstallerSamples/MachineLearning/04_Models/11_KNN_WithScaling.pas @@ -0,0 +1,60 @@ +// KNN чувствителен к масштабу признаков. +// +// В этом примере мы специально делаем часть признаков +// очень большими по масштабу. Без StandardScaler расстояние +// между объектами начинает определяться в основном этими +// признаками, и качество KNN ухудшается. +// +// После масштабирования все признаки снова становятся +// сопоставимыми, и модель работает лучше. +uses MLABC; + +begin + var (X, y) := Datasets.MakeClassification( + n := 500, + nFeatures := 4, + nInformative := 2, + nRedundant := 0, + noise := 0.15, + classSep := 2.2, + flipProb := 0.02, + classBalance := 0.5, + shuffle := True, + seed := 42 + ); + + // Искусственно увеличиваем масштаб двух последних признаков. + // Они начинают слишком сильно влиять на расстояние в KNN. + for var i := 0 to X.RowCount - 1 do + begin + X[i, 2] *= 1000; + X[i, 3] *= 1000; + end; + + var (Xtrain, Xtest, ytrain, ytest) := + Validation.TrainTestSplit(X, y, testRatio := 0.25, seed := 42); + + // --- KNN без масштабирования + var knnRaw := new KNNClassifier(7); + knnRaw.Fit(Xtrain, ytrain); + + var predRaw := knnRaw.Predict(Xtest); + var accRaw := Metrics.Accuracy(ytest, predRaw); + + // --- Масштабируем признаки + var scaler := new StandardScaler; + scaler.Fit(Xtrain); + + var XtrainScaled := scaler.Transform(Xtrain); + var XtestScaled := scaler.Transform(Xtest); + + // --- KNN после масштабирования + var knnScaled := new KNNClassifier(7); + knnScaled.Fit(XtrainScaled, ytrain); + + var predScaled := knnScaled.Predict(XtestScaled); + var accScaled := Metrics.Accuracy(ytest, predScaled); + + Println($'Точность KNN без масштабирования: {accRaw:F3}'); + Println($'Точность KNN после StandardScaler: {accScaled:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/04_Models/12_KNNRegression.pas b/InstallerSamples/MachineLearning/04_Models/12_KNNRegression.pas new file mode 100644 index 000000000..6287b6d21 --- /dev/null +++ b/InstallerSamples/MachineLearning/04_Models/12_KNNRegression.pas @@ -0,0 +1,51 @@ +// В этом примере сравниваются две регрессионные модели: +// линейная регрессия и KNNRegressor. +// +// Данные специально сделаны нелинейными. Для такой задачи +// LinearRegression обычно слишком проста, а KNNRegressor +// может лучше учитывать локальную форму зависимости. +// +// Если R² отрицателен, это означает, что модель работает +// даже хуже, чем очень простой прогноз по среднему значению. + +uses MLABC; + +begin + var (X, y) := Datasets.MakeRegression( + n := 300, + nFeatures := 2, + nInformative := 2, + noise := 0.15, + coefScale := 1.0, + bias := 0.0, + nonlinearStrength := 4.0, + shuffle := True, + seed := 42 + ); + + var (XTrain, XTest, yTrain, yTest) := Validation.TrainTestSplit( + X, y, testRatio := 0.3, seed := 42 + ); + + var linear := new LinearRegression; + linear.Fit(XTrain, yTrain); + + var knn := new KNNRegressor(7, KNNWeighting.Distance); + knn.Fit(XTrain, yTrain); + + var yPredLinear := linear.Predict(XTest); + var yPredKNN := knn.Predict(XTest); + + var r2Linear := RegressionMetrics.R2(yTest, yPredLinear); + var r2KNN := RegressionMetrics.R2(yTest, yPredKNN); + + Println('Сравнение моделей на нелинейной зависимости'); + Println; + Println($'Линейная регрессия: R² = {r2Linear:F3}'); + Println($'KNN-регрессия: R² = {r2KNN:F3}'); + Println; + Println('Интерпретация результата:'); + Println('- Линейная регрессия здесь слишком проста для задачи.'); + Println('- Отрицательный R² означает, что её прогноз хуже среднего значения.'); + Println('- KNN-регрессия лучше описывает локальную нелинейную зависимость.'); +end. diff --git a/InstallerSamples/MachineLearning/04_Models/13_DecisionTreeRegression.pas b/InstallerSamples/MachineLearning/04_Models/13_DecisionTreeRegression.pas new file mode 100644 index 000000000..8ecd7e500 --- /dev/null +++ b/InstallerSamples/MachineLearning/04_Models/13_DecisionTreeRegression.pas @@ -0,0 +1,50 @@ +// В этом примере сравниваются две модели на нелинейной задаче: +// линейная регрессия и дерево решений для регрессии. +// +// Линейная модель умеет строить только одну общую прямую зависимость. +// Дерево решений разбивает пространство признаков на области +// и в каждой области даёт свой локальный прогноз. + +uses MLABC; + +begin + var (X, y) := Datasets.MakeRegression( + n := 300, + nFeatures := 2, + nInformative := 2, + noise := 0.15, + coefScale := 1.0, + bias := 0.0, + nonlinearStrength := 4.0, + shuffle := True, + seed := 42 + ); + + var (XTrain, XTest, yTrain, yTest) := Validation.TrainTestSplit( + X, y, testRatio := 0.3, seed := 42 + ); + + var linear := new LinearRegression; + linear.Fit(XTrain, yTrain); + + // При minSamplesLeaf = 5 узел должен содержать хотя бы 10 объектов, + // чтобы его можно было разделить на два листа не меньше чем по 5 объектов. + var tree := new DecisionTreeRegressor(maxDepth := 6, minSamplesSplit := 10, minSamplesLeaf := 5); + tree.Fit(XTrain, yTrain); + + var yPredLinear := linear.Predict(XTest); + var yPredTree := tree.Predict(XTest); + + var r2Linear := RegressionMetrics.R2(yTest, yPredLinear); + var r2Tree := RegressionMetrics.R2(yTest, yPredTree); + + Println('Сравнение моделей на нелинейной зависимости'); + Println; + Println($'Линейная регрессия: R² = {r2Linear:F3}'); + Println($'Дерево решений: R² = {r2Tree:F3}'); + Println; + Println('Интерпретация результата:'); + Println('- Линейная регрессия пытается описать всю зависимость одной формулой.'); + Println('- Дерево решений умеет подстраиваться под разные участки данных.'); + Println('- Поэтому на нелинейной задаче дерево обычно работает лучше.'); +end. diff --git a/InstallerSamples/MachineLearning/04_Models/14_RandomForestRegression.pas b/InstallerSamples/MachineLearning/04_Models/14_RandomForestRegression.pas new file mode 100644 index 000000000..0b400ae67 --- /dev/null +++ b/InstallerSamples/MachineLearning/04_Models/14_RandomForestRegression.pas @@ -0,0 +1,65 @@ +// В этом примере сравниваются три модели на нелинейной задаче: +// линейная регрессия, дерево решений и случайный лес. +// +// Случайный лес усредняет предсказания многих деревьев. +// Благодаря этому он обычно работает устойчивее и точнее, +// чем одно дерево решений, особенно если в данных есть шум +// и лишние признаки. +// +// В этой задаче для леса используется режим AllFeatures. +// Для регрессии это часто даёт более сильный результат, +// чем случайный выбор только части признаков в каждом узле. + +uses MLABC; + +begin + var (X, y) := Datasets.MakeRegression( + n := 300, + nFeatures := 8, + nInformative := 2, + noise := 0.35, + coefScale := 1.0, + bias := 0.0, + nonlinearStrength := 4.0, + shuffle := True, + seed := 42 + ); + + var (XTrain, XTest, yTrain, yTest) := Validation.TrainTestSplit( + X, y, testRatio := 0.3, seed := 42 + ); + + var linear := new LinearRegression; + linear.Fit(XTrain, yTrain); + + var tree := new DecisionTreeRegressor(maxDepth := 8, minSamplesSplit := 10, minSamplesLeaf := 5); + tree.Fit(XTrain, yTrain); + + var forest := new RandomForestRegressor( + nTrees := 150, + maxDepth := 8, + minSamplesSplit := 10, + minSamplesLeaf := 5, + maxFeaturesMode := TMaxFeaturesMode.AllFeatures + ); + forest.Fit(XTrain, yTrain); + + var yPredLinear := linear.Predict(XTest); + var yPredTree := tree.Predict(XTest); + var yPredForest := forest.Predict(XTest); + + var r2Linear := RegressionMetrics.R2(yTest, yPredLinear); + var r2Tree := RegressionMetrics.R2(yTest, yPredTree); + var r2Forest := RegressionMetrics.R2(yTest, yPredForest); + + Println('Сравнение моделей на нелинейной зависимости'); + Println; + Println($'Линейная регрессия: R² = {r2Linear:F3}'); + Println($'Дерево решений: R² = {r2Tree:F3}'); + Println($'Случайный лес: R² = {r2Forest:F3}'); + Println; + Println('Интерпретация результата:'); + Println('- Линейная модель плохо подходит для сложной нелинейной зависимости.'); + Println('- Одно дерево решений уже может уловить форму данных.'); + Println('- Случайный лес усредняет много деревьев и обычно даёт более устойчивый прогноз.'); +end. diff --git a/InstallerSamples/MachineLearning/04_Models/15_GradientBoostingRegression.pas b/InstallerSamples/MachineLearning/04_Models/15_GradientBoostingRegression.pas new file mode 100644 index 000000000..646a92eb5 --- /dev/null +++ b/InstallerSamples/MachineLearning/04_Models/15_GradientBoostingRegression.pas @@ -0,0 +1,62 @@ +// В этом примере сравниваются три модели на нелинейной задаче: +// линейная регрессия, дерево решений и градиентный бустинг. +// +// Градиентный бустинг строит много небольших деревьев последовательно. +// Каждое следующее дерево старается исправить ошибки предыдущих, +// поэтому на сложных зависимостях такая модель часто работает очень хорошо. + +uses MLABC; + +begin + var (X, y) := Datasets.MakeRegression( + n := 300, + nFeatures := 8, + nInformative := 2, + noise := 0.35, + coefScale := 1.0, + bias := 0.0, + nonlinearStrength := 4.0, + shuffle := True, + seed := 42 + ); + + var (XTrain, XTest, yTrain, yTest) := Validation.TrainTestSplit( + X, y, testRatio := 0.3, seed := 42 + ); + + var linear := new LinearRegression; + linear.Fit(XTrain, yTrain); + + var tree := new DecisionTreeRegressor(maxDepth := 6, minSamplesSplit := 10, minSamplesLeaf := 5); + tree.Fit(XTrain, yTrain); + + var boosting := new GradientBoostingRegressor( + nEstimators := 120, + learningRate := 0.1, + maxDepth := 3, + minSamplesSplit := 10, + minSamplesLeaf := 5, + subsample := 1.0, + seed := 42 + ); + boosting.Fit(XTrain, yTrain); + + var yPredLinear := linear.Predict(XTest); + var yPredTree := tree.Predict(XTest); + var yPredBoosting := boosting.Predict(XTest); + + var r2Linear := RegressionMetrics.R2(yTest, yPredLinear); + var r2Tree := RegressionMetrics.R2(yTest, yPredTree); + var r2Boosting := RegressionMetrics.R2(yTest, yPredBoosting); + + Println('Сравнение моделей на нелинейной зависимости'); + Println; + Println($'Линейная регрессия: R² = {r2Linear:F3}'); + Println($'Дерево решений: R² = {r2Tree:F3}'); + Println($'Градиентный бустинг: R² = {r2Boosting:F3}'); + Println; + Println('Интерпретация результата:'); + Println('- Линейная модель плохо подходит для сложной нелинейной зависимости.'); + Println('- Одно дерево решений уже может уловить форму данных.'); + Println('- Градиентный бустинг последовательно исправляет ошибки и часто даёт самый точный прогноз.'); +end. diff --git a/InstallerSamples/MachineLearning/04_Models/16_RandomForestClassification.pas b/InstallerSamples/MachineLearning/04_Models/16_RandomForestClassification.pas new file mode 100644 index 000000000..c01fa3c99 --- /dev/null +++ b/InstallerSamples/MachineLearning/04_Models/16_RandomForestClassification.pas @@ -0,0 +1,49 @@ +// В этом примере сравниваются две модели на нелинейной задаче классификации: +// LogisticRegression и RandomForestClassifier. +// +// Логистическая регрессия строит линейную границу между классами. +// Поэтому на вложенных окружностях она принципиально ограничена. +// +// Случайный лес умеет учитывать более сложную структуру данных +// и обычно лучше справляется с нелинейной геометрией. + +uses MLABC; + +begin + var (X, y) := Datasets.MakeCircles( + n := 450, + noise := 0.18, + factor := 0.5, + classBalance := 0.5, + flipProb := 0.04, + scale := 3.0, + seed := 42 + ); + + var (XTrain, XTest, yTrain, yTest) := Validation.TrainTestSplit(X, y, 0.25, seed := 42); + + var logreg := new LogisticRegression(learningRate := 0.05, epochs := 1000); + logreg.Fit(XTrain, yTrain); + var logregPred := logreg.Predict(XTest); + var logregAcc := ClassificationMetrics.Accuracy(yTest, logregPred); + + var forest := new RandomForestClassifier( + nTrees := 150, + maxDepth := 8, + minSamplesSplit := 6, + minSamplesLeaf := 3, + seed := 42 + ); + forest.Fit(XTrain, yTrain); + var forestPred := forest.Predict(XTest); + var forestAcc := ClassificationMetrics.Accuracy(yTest, forestPred); + + Println('Сравнение моделей на нелинейной задаче классификации'); + Println; + Println($'Логистическая регрессия: Accuracy = {logregAcc:F3}'); + Println($'Случайный лес: Accuracy = {forestAcc:F3}'); + Println; + Println('Интерпретация результата:'); + Println('- Логистическая регрессия строит только линейную границу.'); + Println('- Случайный лес лучше улавливает нелинейную форму классов.'); +end. diff --git a/InstallerSamples/MachineLearning/04_Models/17_GradientBoostingClassification.pas b/InstallerSamples/MachineLearning/04_Models/17_GradientBoostingClassification.pas new file mode 100644 index 000000000..bb88a9488 --- /dev/null +++ b/InstallerSamples/MachineLearning/04_Models/17_GradientBoostingClassification.pas @@ -0,0 +1,44 @@ +// В этом примере сравниваются две модели на нелинейной задаче классификации: +// LogisticRegression и GradientBoostingClassifier. +// +// Логистическая регрессия строит линейную границу между классами. +// Градиентный бустинг последовательно улучшает ансамбль деревьев +// и хорошо справляется со сложной формой границы. + +uses MLABC; + +begin + var (X, y) := Datasets.MakeMoons( + n := 450, + noise := 0.20, + seed := 42 + ); + + var (XTrain, XTest, yTrain, yTest) := Validation.TrainTestSplit(X, y, 0.25, seed := 42); + + var logreg := new LogisticRegression(learningRate := 0.05, epochs := 1000); + logreg.Fit(XTrain, yTrain); + var logregPred := logreg.Predict(XTest); + var logregAcc := ClassificationMetrics.Accuracy(yTest, logregPred); + + var gb := new GradientBoostingClassifier( + nEstimators := 120, + learningRate := 0.1, + maxDepth := 3, + minSamplesSplit := 6, + minSamplesLeaf := 3, + seed := 42 + ); + gb.Fit(XTrain, yTrain); + var gbPred := gb.Predict(XTest); + var gbAcc := ClassificationMetrics.Accuracy(yTest, gbPred); + + Println('Сравнение моделей на нелинейной задаче классификации'); + Println; + Println($'Логистическая регрессия: Accuracy = {logregAcc:F3}'); + Println($'Градиентный бустинг: Accuracy = {gbAcc:F3}'); + Println; + Println('Интерпретация результата:'); + Println('- Логистическая регрессия строит только линейную границу.'); + Println('- Градиентный бустинг лучше улавливает сложную форму классов.'); +end. diff --git a/InstallerSamples/MachineLearning/04_Models/18_DecisionTreeClassification.pas b/InstallerSamples/MachineLearning/04_Models/18_DecisionTreeClassification.pas new file mode 100644 index 000000000..148c719b3 --- /dev/null +++ b/InstallerSamples/MachineLearning/04_Models/18_DecisionTreeClassification.pas @@ -0,0 +1,32 @@ +// В этом примере сравниваются две модели на нелинейной задаче классификации: +// LogisticRegression и DecisionTreeClassifier. + +uses MLABC; + +begin + var (X, y) := Datasets.MakeMoons( + n := 450, + noise := 0.20, + seed := 42 + ); + + var (XTrain, XTest, yTrain, yTest) := Validation.TrainTestSplit(X, y, 0.25, seed := 42); + + var logreg := new LogisticRegression(learningRate := 0.05, epochs := 1000); + logreg.Fit(XTrain, yTrain); + var logregAcc := ClassificationMetrics.Accuracy(yTest, logreg.Predict(XTest)); + + var tree := new DecisionTreeClassifier( + maxDepth := 5, + minSamplesSplit := 6, + minSamplesLeaf := 3, + seed := 42 + ); + tree.Fit(XTrain, yTrain); + var treeAcc := ClassificationMetrics.Accuracy(yTest, tree.Predict(XTest)); + + Println('Сравнение моделей на нелинейной задаче классификации'); + Println; + Println($'Логистическая регрессия: Accuracy = {logregAcc:F3}'); + Println($'Дерево решений: Accuracy = {treeAcc:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/04_Models/19_KNNClassification.pas b/InstallerSamples/MachineLearning/04_Models/19_KNNClassification.pas new file mode 100644 index 000000000..12bead154 --- /dev/null +++ b/InstallerSamples/MachineLearning/04_Models/19_KNNClassification.pas @@ -0,0 +1,27 @@ +// В этом примере сравниваются две модели на нелинейной задаче классификации: +// LogisticRegression и KNNClassifier. + +uses MLABC; + +begin + var (X, y) := Datasets.MakeMoons( + n := 450, + noise := 0.20, + seed := 42 + ); + + var (XTrain, XTest, yTrain, yTest) := Validation.TrainTestSplit(X, y, 0.25, seed := 42); + + var logreg := new LogisticRegression(learningRate := 0.05, epochs := 1000); + logreg.Fit(XTrain, yTrain); + var logregAcc := ClassificationMetrics.Accuracy(yTest, logreg.Predict(XTest)); + + var knn := new KNNClassifier(7, KNNWeighting.Distance); + knn.Fit(XTrain, yTrain); + var knnAcc := ClassificationMetrics.Accuracy(yTest, knn.Predict(XTest)); + + Println('Сравнение моделей на нелинейной задаче классификации'); + Println; + Println($'Логистическая регрессия: Accuracy = {logregAcc:F3}'); + Println($'KNNClassifier: Accuracy = {knnAcc:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/04_Models/20_LogisticRegression_Probabilities.pas b/InstallerSamples/MachineLearning/04_Models/20_LogisticRegression_Probabilities.pas new file mode 100644 index 000000000..42c12c779 --- /dev/null +++ b/InstallerSamples/MachineLearning/04_Models/20_LogisticRegression_Probabilities.pas @@ -0,0 +1,36 @@ +// В этом примере LogisticRegression возвращает не только класс, +// но и вероятности принадлежности к каждому классу. + +uses MLABC; + +begin + var ds := Datasets.Iris; + var (trainDs, testDs) := ds.StratifiedTrainTestSplit(testRatio := 0.2, seed := 42); + + var pipe := + DataPipeline.Build( + TaskKind.tkClassification, + ds.Target, + ds.Features, + new StandardScaler, + new LogisticRegression(learningRate := 0.01, epochs := 2000) + ); + + pipe.Fit(trainDs.Data); + + var pred := pipe.Predict(testDs.Data); + var yTest := pipe.GetEncodedLabels(testDs.Data); + var acc := ClassificationMetrics.Accuracy(yTest, pred); + + var proba := pipe.PredictProba(testDs.Data); + var classes := pipe.GetClassLabels; + + Println('Логистическая регрессия с вероятностями классов'); + Println; + Println($'Точность: {acc:F3}'); + Println; + Println('Вероятности классов для первого объекта тестовой выборки:'); + + for var j := 0 to classes.Length - 1 do + Println($' {classes[j]}: {proba[0, j]:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/04_Models/21_RandomForest_Probabilities.pas b/InstallerSamples/MachineLearning/04_Models/21_RandomForest_Probabilities.pas new file mode 100644 index 000000000..f21863fd8 --- /dev/null +++ b/InstallerSamples/MachineLearning/04_Models/21_RandomForest_Probabilities.pas @@ -0,0 +1,35 @@ +// В этом примере RandomForestClassifier возвращает +// вероятности принадлежности к каждому классу. + +uses MLABC; + +begin + var ds := Datasets.Iris; + var (trainDs, testDs) := ds.StratifiedTrainTestSplit(testRatio := 0.2, seed := 42); + + var pipe := + DataPipeline.Build( + TaskKind.tkClassification, + ds.Target, + ds.Features, + new RandomForestClassifier(nTrees := 100, maxDepth := 6, seed := 42) + ); + + pipe.Fit(trainDs.Data); + + var pred := pipe.Predict(testDs.Data); + var yTest := pipe.GetEncodedLabels(testDs.Data); + var acc := ClassificationMetrics.Accuracy(yTest, pred); + + var proba := pipe.PredictProba(testDs.Data); + var classes := pipe.GetClassLabels; + + Println('Случайный лес с вероятностями классов'); + Println; + Println($'Точность: {acc:F3}'); + Println; + Println('Вероятности классов для первого объекта тестовой выборки:'); + + for var j := 0 to classes.Length - 1 do + Println($' {classes[j]}: {proba[0, j]:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/04_Models/22_GradientBoosting_Probabilities.pas b/InstallerSamples/MachineLearning/04_Models/22_GradientBoosting_Probabilities.pas new file mode 100644 index 000000000..e2f2b6c2f --- /dev/null +++ b/InstallerSamples/MachineLearning/04_Models/22_GradientBoosting_Probabilities.pas @@ -0,0 +1,42 @@ +// В этом примере GradientBoostingClassifier возвращает +// вероятности принадлежности к каждому классу. + +uses MLABC; + +begin + var ds := Datasets.Iris; + var (trainDs, testDs) := ds.StratifiedTrainTestSplit(testRatio := 0.2, seed := 42); + + var pipe := + DataPipeline.Build( + TaskKind.tkClassification, + ds.Target, + ds.Features, + new GradientBoostingClassifier( + nEstimators := 80, + learningRate := 0.1, + maxDepth := 3, + minSamplesSplit := 6, + minSamplesLeaf := 3, + seed := 42 + ) + ); + + pipe.Fit(trainDs.Data); + + var pred := pipe.Predict(testDs.Data); + var yTest := pipe.GetEncodedLabels(testDs.Data); + var acc := ClassificationMetrics.Accuracy(yTest, pred); + + var proba := pipe.PredictProba(testDs.Data); + var classes := pipe.GetClassLabels; + + Println('Градиентный бустинг с вероятностями классов'); + Println; + Println($'Точность: {acc:F3}'); + Println; + Println('Вероятности классов для первого объекта тестовой выборки:'); + + for var j := 0 to classes.Length - 1 do + Println($' {classes[j]}: {proba[0, j]:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/05_Validation/01_TrainTestSplit_CrossValidate.pas b/InstallerSamples/MachineLearning/05_Validation/01_TrainTestSplit_CrossValidate.pas new file mode 100644 index 000000000..da25dc558 --- /dev/null +++ b/InstallerSamples/MachineLearning/05_Validation/01_TrainTestSplit_CrossValidate.pas @@ -0,0 +1,47 @@ +// В этом примере одна и та же модель оценивается двумя способами: +// 1. по одной обучающей и тестовой выборке; +// 2. по стратифицированной k-fold кросс-валидации. +// +// Один train/test split даёт быструю, но более случайную оценку. +// Кросс-валидация обычно надёжнее, потому что усредняет результат +// по нескольким разбиениям данных. + +uses MLABC; + +begin + var ds := Datasets.Iris; + var (trainDs, testDs) := ds.StratifiedTrainTestSplit(testRatio := 0.2, seed := 3); + + var XTrain := trainDs.Data.ToMatrix(trainDs.Features); + var yTrain := trainDs.Data.EncodeLabels(trainDs.Target); + + var XTest := testDs.Data.ToMatrix(testDs.Features); + var yTest := testDs.Data.EncodeLabels(testDs.Target); + + var X := ds.Data.ToMatrix(ds.Features); + var y := ds.Data.EncodeLabels(ds.Target); + + var model := new LogisticRegression; + model.Fit(XTrain, yTrain); + + var yPred := model.Predict(XTest); + var testAccuracy := Metrics.Accuracy(yTest, yPred); + + var cvAccuracy := Validation.StratifiedCrossValidate( + new LogisticRegression, + X, y, + 5, + Metrics.Accuracy, + seed := 1 + ); + + Println('Оценка одной и той же модели двумя способами'); + Println; + Println($'Точность на стратифицированной тестовой выборке: {testAccuracy:F3}'); + Println($'Средняя точность по кросс-валидации: {cvAccuracy:F3}'); + Println; + Println('Интерпретация результата:'); + Println('- Одна тестовая выборка даёт быстрый, но более случайный результат.'); + Println('- Кросс-валидация усредняет качество по нескольким разбиениям.'); + Println('- Поэтому её часто используют для более надёжной оценки модели.'); +end. diff --git a/InstallerSamples/MachineLearning/05_Validation/02_CrossValidate_Regression.pas b/InstallerSamples/MachineLearning/05_Validation/02_CrossValidate_Regression.pas new file mode 100644 index 000000000..7fffefca5 --- /dev/null +++ b/InstallerSamples/MachineLearning/05_Validation/02_CrossValidate_Regression.pas @@ -0,0 +1,46 @@ +// В этом примере линейная регрессия оценивается двумя способами: +// 1. по одной обучающей и тестовой выборке; +// 2. по k-fold кросс-валидации. +// +// Для регрессии в качестве метрики используется R²: +// чем ближе значение к 1, тем лучше модель объясняет данные. + +uses MLABC; + +begin + var ds := Datasets.MoscowHousing; + var df := ds.Data; + + var features := ['rooms', 'area', 'kitchen_area', 'floor', 'floors_total', 'metro_minutes']; + var target := 'price'; + + var X := df.ToMatrix(features); + var y := df.ToVector(target); + + var (XTrain, XTest, yTrain, yTest) := + Validation.TrainTestSplit(X, y, testRatio := 0.2, seed := 1); + + var model := new LinearRegression; + model.Fit(XTrain, yTrain); + + var yPred := model.Predict(XTest); + var testR2 := RegressionMetrics.R2(yTest, yPred); + + var cvR2 := Validation.CrossValidate( + new LinearRegression, + X, y, + 5, + RegressionMetrics.R2, + 1 + ); + + Println('Оценка линейной регрессии двумя способами'); + Println; + Println($'R² на тестовой выборке: {testR2:F3}'); + Println($'Средний R² по кросс-валидации: {cvR2:F3}'); + Println; + Println('Интерпретация результата:'); + Println('- Одна тестовая выборка даёт быструю оценку качества.'); + Println('- Кросс-валидация усредняет результат по нескольким разбиениям.'); + Println('- Если значения близки, модель ведёт себя достаточно стабильно.'); +end. diff --git a/InstallerSamples/MachineLearning/05_Validation/03_GridSearch_Ridge.pas b/InstallerSamples/MachineLearning/05_Validation/03_GridSearch_Ridge.pas new file mode 100644 index 000000000..d3d5590fc --- /dev/null +++ b/InstallerSamples/MachineLearning/05_Validation/03_GridSearch_Ridge.pas @@ -0,0 +1,81 @@ +// В этом примере подбирается параметр регуляризации для RidgeRegression. +// +// Используются линейные данные с сильно коррелированными признаками. +// В такой ситуации Ridge часто работает устойчивее обычной линейной регрессии. +// +// Для подбора используется метрика MSE: +// чем меньше значение, тем лучше модель. + +uses MLABC; +uses System; + +function Normal(rnd: Random): real; +begin + var u1 := rnd.NextDouble; + var u2 := rnd.NextDouble; + if u1 < 1e-12 then + u1 := 1e-12; + Result := Sqrt(-2 * Ln(u1)) * Cos(2 * Pi * u2); +end; + +begin + var (X, y) := Datasets.MakeRegression( + n := 100, + nFeatures := 20, + nInformative := 3, + noise := 2.0, + coefScale := 1.0, + bias := 0.0, + nonlinearStrength := 0.0, + shuffle := True, + seed := 42 + ); + + // Несколько признаков делаем почти копиями первого, + // чтобы усилить корреляцию и сделать Ridge полезным. + var rnd := new Random(1); + for var i := 0 to X.RowCount - 1 do + begin + X[i, 1] := X[i, 0] + 0.01 * Normal(rnd); + X[i, 2] := X[i, 0] - 0.01 * Normal(rnd); + end; + + var lambdaValues := [0.0, 1.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0]; + + Println('Подбор параметра для RidgeRegression'); + Println; + Println('Проверяемые значения lambda:'); + + foreach var lambda in lambdaValues do + begin + var score := Validation.CrossValidate( + new RidgeRegression(lambda), + X, y, + 5, + RegressionMetrics.MSE, + 1 + ); + Println($' lambda = {lambda,6:F2} -> средняя MSE = {score:F3}'); + end; + + Println; + + var (bestLambda, bestScore, bestModel) := GridSearch.Search( + lambda -> new RidgeRegression(lambda), + lambdaValues, + X, y, + 5, + RegressionMetrics.MSE, + maximize := False, + stratified := False, + seed := 1 + ); + + Println($'Лучшее значение lambda: {bestLambda:F2}'); + Println($'Лучшая средняя MSE: {bestScore:F3}'); + Println; + Println('Интерпретация результата:'); + Println('- GridSearch перебирает несколько значений параметра lambda.'); + Println('- Для каждого значения качество оценивается по кросс-валидации.'); + Println('- Лучшим считается параметр с наименьшей средней ошибкой MSE.'); +end. diff --git a/InstallerSamples/MachineLearning/05_Validation/04_GridSearch_KNN.pas b/InstallerSamples/MachineLearning/05_Validation/04_GridSearch_KNN.pas new file mode 100644 index 000000000..1ac9589f6 --- /dev/null +++ b/InstallerSamples/MachineLearning/05_Validation/04_GridSearch_KNN.pas @@ -0,0 +1,50 @@ +// В этом примере подбирается число соседей k +// для KNNClassifier с помощью GridSearch. + +uses MLABC; + +begin + var (X, y) := Datasets.MakeMoons( + n := 400, + noise := 0.18, + seed := 42 + ); + + var scaler := new StandardScaler; + scaler.Fit(X); + X := scaler.Transform(X); + + var kValues := [1, 3, 5, 7, 9, 11, 15]; + + Println('Подбор параметра k для KNNClassifier'); + Println; + Println('Проверяемые значения k:'); + + foreach var k in kValues do + begin + var score := Validation.StratifiedCrossValidate( + new KNNClassifier(k, KNNWeighting.Distance), + X, y, + 5, + ClassificationMetrics.Accuracy, + seed := 42 + ); + + Println($' k = {k,2} -> средняя Accuracy = {score:F3}'); + end; + + var (bestK, bestScore, bestModel) := GridSearch.Search( + k -> new KNNClassifier(k, KNNWeighting.Distance), + kValues, + X, y, + 5, + ClassificationMetrics.Accuracy, + maximize := True, + stratified := True, + seed := 42 + ); + + Println; + Println($'Лучшее значение k: {bestK}'); + Println($'Лучшая средняя Accuracy: {bestScore:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/05_Validation/05_GridSearch_LogisticRegression.exe b/InstallerSamples/MachineLearning/05_Validation/05_GridSearch_LogisticRegression.exe new file mode 100644 index 000000000..6f6ef6337 Binary files /dev/null and b/InstallerSamples/MachineLearning/05_Validation/05_GridSearch_LogisticRegression.exe differ diff --git a/InstallerSamples/MachineLearning/05_Validation/05_GridSearch_LogisticRegression.pas b/InstallerSamples/MachineLearning/05_Validation/05_GridSearch_LogisticRegression.pas new file mode 100644 index 000000000..d0bdf60b8 --- /dev/null +++ b/InstallerSamples/MachineLearning/05_Validation/05_GridSearch_LogisticRegression.pas @@ -0,0 +1,56 @@ +// В этом примере подбирается коэффициент регуляризации +// для LogisticRegression с помощью GridSearch. + +uses MLABC; + +begin + var (X, y) := Datasets.MakeClassification( + n := 400, + nFeatures := 6, + nInformative := 3, + nRedundant := 1, + noise := 0.2, + classSep := 1.0, + flipProb := 0.06, + seed := 42 + ); + + var scaler := new StandardScaler; + scaler.Fit(X); + X := scaler.Transform(X); + + var lambdaValues := [0.0, 0.01, 0.1, 0.5, 1.0, 2.0, 5.0]; + + Println('Подбор параметра lambda для LogisticRegression'); + Println; + Println('Проверяемые значения lambda:'); + + foreach var lambda in lambdaValues do + begin + var score := Validation.StratifiedCrossValidate( + new LogisticRegression(lambda := lambda, learningRate := 0.05, epochs := 1000), + X, y, + 5, + ClassificationMetrics.Accuracy, + seed := 42 + ); + + Println($' lambda = {lambda,4:F2} -> средняя Accuracy = {score:F3}'); + end; + + Println; + + var (bestLambda, bestScore, bestModel) := GridSearch.Search( + lambda -> new LogisticRegression(lambda := lambda, learningRate := 0.05, epochs := 1000), + lambdaValues, + X, y, + 5, + ClassificationMetrics.Accuracy, + maximize := True, + stratified := True, + seed := 42 + ); + + Println($'Лучшее значение lambda: {bestLambda:F2}'); + Println($'Лучшая средняя Accuracy: {bestScore:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/05_Validation/06_ModelComparison_Classification.pas b/InstallerSamples/MachineLearning/05_Validation/06_ModelComparison_Classification.pas new file mode 100644 index 000000000..09f4ce371 --- /dev/null +++ b/InstallerSamples/MachineLearning/05_Validation/06_ModelComparison_Classification.pas @@ -0,0 +1,52 @@ +// В этом примере сравниваются несколько моделей +// на одной задаче классификации. + +uses MLABC; + +begin + var (X, y) := Datasets.MakeMoons( + n := 400, + noise := 0.18, + seed := 42 + ); + + Println('Сравнение моделей классификации по кросс-валидации'); + Println; + + var logregScore := Validation.StratifiedCrossValidate( + new LogisticRegression(learningRate := 0.05, epochs := 1000), + X, y, + 5, + ClassificationMetrics.Accuracy, + seed := 42 + ); + + var treeScore := Validation.StratifiedCrossValidate( + new DecisionTreeClassifier(maxDepth := 5, minSamplesSplit := 6, minSamplesLeaf := 3, seed := 42), + X, y, + 5, + ClassificationMetrics.Accuracy, + seed := 42 + ); + + var forestScore := Validation.StratifiedCrossValidate( + new RandomForestClassifier(nTrees := 100, maxDepth := 6, minSamplesSplit := 6, minSamplesLeaf := 3, seed := 42), + X, y, + 5, + ClassificationMetrics.Accuracy, + seed := 42 + ); + + var gbScore := Validation.StratifiedCrossValidate( + new GradientBoostingClassifier(nEstimators := 80, learningRate := 0.1, maxDepth := 3, minSamplesSplit := 6, minSamplesLeaf := 3, seed := 42), + X, y, + 5, + ClassificationMetrics.Accuracy, + seed := 42 + ); + + Println($'LogisticRegression: Accuracy = {logregScore:F3}'); + Println($'DecisionTreeClassifier: Accuracy = {treeScore:F3}'); + Println($'RandomForestClassifier: Accuracy = {forestScore:F3}'); + Println($'GradientBoostingClassifier: Accuracy = {gbScore:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/05_Validation/07_ModelComparison_Regression.pas b/InstallerSamples/MachineLearning/05_Validation/07_ModelComparison_Regression.pas new file mode 100644 index 000000000..15dc822cf --- /dev/null +++ b/InstallerSamples/MachineLearning/05_Validation/07_ModelComparison_Regression.pas @@ -0,0 +1,55 @@ +// В этом примере сравниваются несколько моделей +// на одной задаче регрессии. + +uses MLABC; + +begin + var (X, y) := Datasets.MakeRegression( + n := 400, + nFeatures := 8, + nInformative := 3, + noise := 0.35, + nonlinearStrength := 1.5, + seed := 42 + ); + + Println('Сравнение моделей регрессии по кросс-валидации'); + Println; + + var linScore := Validation.CrossValidate( + new LinearRegression, + X, y, + 5, + RegressionMetrics.R2, + seed := 42 + ); + + var treeScore := Validation.CrossValidate( + new DecisionTreeRegressor(maxDepth := 6, minSamplesSplit := 10, minSamplesLeaf := 5, seed := 42), + X, y, + 5, + RegressionMetrics.R2, + seed := 42 + ); + + var forestScore := Validation.CrossValidate( + new RandomForestRegressor(nTrees := 120, maxDepth := 8, minSamplesSplit := 10, minSamplesLeaf := 5, seed := 42), + X, y, + 5, + RegressionMetrics.R2, + seed := 42 + ); + + var gbScore := Validation.CrossValidate( + new GradientBoostingRegressor(nEstimators := 100, learningRate := 0.1, maxDepth := 3, minSamplesSplit := 10, minSamplesLeaf := 5, seed := 42), + X, y, + 5, + RegressionMetrics.R2, + seed := 42 + ); + + Println($'LinearRegression: R² = {linScore:F3}'); + Println($'DecisionTreeRegressor: R² = {treeScore:F3}'); + Println($'RandomForestRegressor: R² = {forestScore:F3}'); + Println($'GradientBoostingRegressor: R² = {gbScore:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/06_Pipelines/01_DataPipeline_Classification.pas b/InstallerSamples/MachineLearning/06_Pipelines/01_DataPipeline_Classification.pas new file mode 100644 index 000000000..e8bd15e89 --- /dev/null +++ b/InstallerSamples/MachineLearning/06_Pipelines/01_DataPipeline_Classification.pas @@ -0,0 +1,43 @@ +// Строго типизированный pipeline для классификации на DataFrame. +// +// Pipeline сам: +// • проверяет схему таблицы; +// • применяет preprocessing к признакам; +// • кодирует целевую переменную внутри себя; +// • передаёт матрицу признаков в модель. +// +// Здесь показан канонический сценарий: +// Iris -> Train/Test Split -> StandardScaler -> LogisticRegression. +uses MLABC; + +begin + var ds := Datasets.Iris; + var df := ds.Data; + + var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 3); + + var pipe := + DataPipeline.Build( + TaskKind.tkClassification, + ds.Target, + ds.Features, + new StandardScaler, + new LogisticRegression + ); + + pipe.Fit(trainDf); + + var pred := pipe.Predict(testDf); + var y := pipe.GetEncodedLabels(testDf); + + Println('Точность:', Metrics.Accuracy(y, pred):0:3); + + var proba := pipe.PredictProba(testDf); + var classes := pipe.GetClassLabels; + + Println('Классы:', classes.JoinToString(', ')); + Println('Вероятности для первого объекта:'); + + for var j := 0 to classes.Length - 1 do + Println(classes[j], ': ', proba[0, j]:0:3); +end. diff --git a/InstallerSamples/MachineLearning/06_Pipelines/02_DataPipeline_Regression.pas b/InstallerSamples/MachineLearning/06_Pipelines/02_DataPipeline_Regression.pas new file mode 100644 index 000000000..ed36a3966 --- /dev/null +++ b/InstallerSamples/MachineLearning/06_Pipelines/02_DataPipeline_Regression.pas @@ -0,0 +1,35 @@ +// Pipeline для регрессии на DataFrame с категориальным признаком. +// +// В этом примере: +// • числовые признаки подаются в модель напрямую; +// • категориальный признак renovation кодируется через OneHotEncoder; +// • затем все признаки масштабируются; +// • после этого обучается линейная регрессия. +uses MLABC; + +begin + var ds := Datasets.MoscowHousing; + var df := ds.Data; + + var features := ['rooms', 'area', 'kitchen_area', 'floor', 'floors_total', 'metro_minutes', 'renovation']; + var target := 'price'; + + var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 42); + + var pipe := + DataPipeline.Build( + TaskKind.tkRegression, + target, + features, + new OneHotEncoder('renovation'), + new StandardScaler, + new LinearRegression + ); + + pipe.Fit(trainDf); + + var pred := pipe.Predict(testDf); + var y := testDf.ToVector(target); + + Println($'R²: {Metrics.R2(y, pred):F3}'); +end. diff --git a/InstallerSamples/MachineLearning/06_Pipelines/02_DataPipeline_Regression_Categorical.exe b/InstallerSamples/MachineLearning/06_Pipelines/02_DataPipeline_Regression_Categorical.exe new file mode 100644 index 000000000..cc1320094 Binary files /dev/null and b/InstallerSamples/MachineLearning/06_Pipelines/02_DataPipeline_Regression_Categorical.exe differ diff --git a/InstallerSamples/MachineLearning/06_Pipelines/03_UDataPipeline_KMeans.pas b/InstallerSamples/MachineLearning/06_Pipelines/03_UDataPipeline_KMeans.pas new file mode 100644 index 000000000..00c6f7d86 --- /dev/null +++ b/InstallerSamples/MachineLearning/06_Pipelines/03_UDataPipeline_KMeans.pas @@ -0,0 +1,48 @@ +// В этом примере показан unsupervised pipeline на DataFrame. +// +// Мы сначала готовим признаки в таблице, +// а затем передаём их в UDataPipeline: +// StandardScaler -> KMeans. +// +// Такой конвейер удобен, когда нужно явно сохранить +// шаги подготовки данных и кластеризации в одном месте. + +uses MLABC; + +begin + var ds := Datasets.RussianCities; + var df := ds.Data; + + // Добавляем два полезных признака для кластеризации городов. + df := df.WithColumnFloat('density', row -> row.Float('population') / row.Float('area')); + df := df.WithColumnFloat('log_population', row -> Ln(row.Float('population'))); + + var features := ['log_population', 'density']; + + var pipe := + UDataPipeline.Build( + features, + new StandardScaler, + new KMeans(3, seed := 42) + ); + + var labels := pipe.FitPredict(df); + df.AddIntColumn('cluster', labels, nil); + + Println('Кластеризация городов с помощью UDataPipeline'); + Println; + Println('Используемые признаки: log_population, density'); + Println('Число найденных кластеров: 3'); + + for var cluster := 0 to 2 do + begin + Println; + Println($'Кластер {cluster + 1}:'); + + df.Filter(row -> row.Int('cluster') = cluster) + .SortBy('population', descending := True) + .Select(['city', 'population', 'density']) + .Head(5) + .Print; + end; +end. diff --git a/InstallerSamples/MachineLearning/06_Pipelines/04_UMatrixPipeline_KMeans.pas b/InstallerSamples/MachineLearning/06_Pipelines/04_UMatrixPipeline_KMeans.pas new file mode 100644 index 000000000..021df6ad8 --- /dev/null +++ b/InstallerSamples/MachineLearning/06_Pipelines/04_UMatrixPipeline_KMeans.pas @@ -0,0 +1,58 @@ +// В этом примере показан unsupervised pipeline для матрицы признаков. +// +// Данные сначала извлекаются из DataFrame в матрицу, +// а затем pipeline выполняет масштабирование и кластеризацию. +// +// Это удобно, когда табличная подготовка уже сделана заранее +// и хочется работать сразу с числовой матрицей. + +uses MLABC; + +begin + var ds := Datasets.RussianCities; + var df := ds.Data; + + // Добавляем два полезных числовых признака для кластеризации городов. + df := df.WithColumnFloat('density', row -> row.Float('population') / row.Float('area')); + df := df.WithColumnFloat('log_population', row -> Ln(row.Float('population'))); + + // Для кластеризации берем два числовых признака: + // логарифм населения и плотность населения. + var features := ['log_population', 'density']; + var X := df.ToMatrix(features); + + // Строим matrix pipeline: + // сначала StandardScaler, затем KMeans. + var pipe := UMatrixPipeline.Build( + new StandardScaler, + new KMeans(3, seed := 42) + ); + + // Обучаем pipeline и получаем метки кластеров. + pipe.Fit(X); + var labels := LabelsToInts(pipe.Predict(X)); + + // Добавляем метки кластеров в таблицу, + // чтобы потом удобно вывести представителей каждого кластера. + df.AddIntColumn('cluster', labels, nil); + + Println('Кластеризация городов с помощью UMatrixPipeline'); + Println; + Println('Используются признаки log_population и density.'); + Println('Для каждого кластера выводятся самые крупные города.'); + Println; + + for var cluster := 0 to 2 do + begin + Println($'Кластер {cluster + 1}:'); + + var clusterDf := df + .Filter(r -> r.Int('cluster') = cluster) + .SortBy('population', descending := True) + .Select(['city', 'population', 'density']) + .Head(5); + + clusterDf.Print; + Println; + end; +end. diff --git a/InstallerSamples/MachineLearning/06_Pipelines/05_MatrixPipeline_Classification.exe b/InstallerSamples/MachineLearning/06_Pipelines/05_MatrixPipeline_Classification.exe new file mode 100644 index 000000000..bbfcd2fb1 Binary files /dev/null and b/InstallerSamples/MachineLearning/06_Pipelines/05_MatrixPipeline_Classification.exe differ diff --git a/InstallerSamples/MachineLearning/06_Pipelines/05_MatrixPipeline_Classification.pas b/InstallerSamples/MachineLearning/06_Pipelines/05_MatrixPipeline_Classification.pas new file mode 100644 index 000000000..9f924d85b --- /dev/null +++ b/InstallerSamples/MachineLearning/06_Pipelines/05_MatrixPipeline_Classification.pas @@ -0,0 +1,45 @@ +// В этом примере показан supervised pipeline для матрицы признаков. +// +// Данные сначала преобразуются в матрицу X и вектор меток y, +// а затем pipeline выполняет масштабирование и обучение модели. +// +// Такой вариант удобен, когда признаки уже подготовлены +// и не требуется отдельная обработка DataFrame. + +uses MLABC; + +begin + var ds := Datasets.Iris; + var df := ds.Data; + + // Извлекаем числовую матрицу признаков + // и кодируем названия классов в числа. + var X := df.ToMatrix(ds.Features); + var y := df.EncodeLabels(ds.Target); + + // Делим данные на обучающую и тестовую выборки. + var (trainDs, testDs) := ds.StratifiedTrainTestSplit(testRatio := 0.3, seed := 42); + + // Строим pipeline: + // StandardScaler -> LogisticRegression. + var pipe := MatrixPipeline.Build( + new StandardScaler, + new LogisticRegression(learningRate := 0.05, epochs := 1000) + ); + + // Обучаем pipeline и проверяем качество на тестовой выборке. + var XTrain := trainDs.Data.ToMatrix(trainDs.Features); + var classes: array of string; + var yTrain := trainDs.Data.EncodeLabels(trainDs.Target, classes); + + var XTest := testDs.Data.ToMatrix(testDs.Features); + var yTest := testDs.Data.TransformLabels(testDs.Target, classes); + + pipe.Fit(XTrain, yTrain); + var pred := pipe.Predict(XTest); + var acc := ClassificationMetrics.Accuracy(yTest, pred); + + Println('Классификация Iris с помощью MatrixPipeline'); + Println; + Println($'Точность на тестовой выборке: {acc:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/06_Pipelines/06_MatrixPipeline_Regression.exe b/InstallerSamples/MachineLearning/06_Pipelines/06_MatrixPipeline_Regression.exe new file mode 100644 index 000000000..73b6cd50c Binary files /dev/null and b/InstallerSamples/MachineLearning/06_Pipelines/06_MatrixPipeline_Regression.exe differ diff --git a/InstallerSamples/MachineLearning/06_Pipelines/06_MatrixPipeline_Regression.pas b/InstallerSamples/MachineLearning/06_Pipelines/06_MatrixPipeline_Regression.pas new file mode 100644 index 000000000..173ecef62 --- /dev/null +++ b/InstallerSamples/MachineLearning/06_Pipelines/06_MatrixPipeline_Regression.pas @@ -0,0 +1,32 @@ +// В этом примере MatrixPipeline используется +// для задачи регрессии. + +uses MLABC; + +begin + var ds := Datasets.MoscowHousing; + + var features := ['area', 'rooms', 'floor', 'max_floor', 'distance_to_metro', 'building_age']; + + var (trainDs, testDs) := ds.TrainTestSplit(testRatio := 0.2, seed := 42); + + var XTrain := trainDs.Data.ToMatrix(features); + var yTrain := trainDs.Data.ToVector(ds.Target); + + var XTest := testDs.Data.ToMatrix(features); + var yTest := testDs.Data.ToVector(ds.Target); + + var pipe := + MatrixPipeline.Build( + new StandardScaler, + new LinearRegression + ); + + pipe.Fit(XTrain, yTrain); + var pred := pipe.Predict(XTest); + var r2 := RegressionMetrics.R2(yTest, pred); + + Println('MatrixPipeline для задачи регрессии'); + Println; + Println($'R² на тестовой выборке: {r2:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/06_Pipelines/07_MatrixPipeline_KNN_WithScaling.pas b/InstallerSamples/MachineLearning/06_Pipelines/07_MatrixPipeline_KNN_WithScaling.pas new file mode 100644 index 000000000..3f73ae425 --- /dev/null +++ b/InstallerSamples/MachineLearning/06_Pipelines/07_MatrixPipeline_KNN_WithScaling.pas @@ -0,0 +1,47 @@ +// В этом примере сравнивается KNNClassifier +// без масштабирования и с MatrixPipeline. +// +// Важно: в pipeline шаг StandardScaler выполняется автоматически. +// То есть MatrixPipeline здесь эквивалентен цепочке: +// StandardScaler -> KNNClassifier. + +uses MLABC; + +begin + var (X, y) := Datasets.MakeClassification( + n := 400, + nFeatures := 4, + nInformative := 2, + nRedundant := 0, + noise := 0.15, + classSep := 1.0, + seed := 42 + ); + + for var i := 0 to X.RowCount - 1 do + begin + X[i, 2] := X[i, 2] * 100; + X[i, 3] := X[i, 3] * 1000; + end; + + var (XTrain, XTest, yTrain, yTest) := Validation.TrainTestSplit(X, y, 0.25, seed := 42); + + var knn := new KNNClassifier(5); + knn.Fit(XTrain, yTrain); + var acc1 := ClassificationMetrics.Accuracy(yTest, knn.Predict(XTest)); + + // Здесь масштабирование выполняется внутри pipeline автоматически. + var pipe := + MatrixPipeline.Build( + new StandardScaler, + new KNNClassifier(5) + ); + + pipe.Fit(XTrain, yTrain); + var acc2 := ClassificationMetrics.Accuracy(yTest, pipe.Predict(XTest)); + + Println('KNNClassifier без масштабирования и с масштабированием'); + Println; + Println($'Без масштабирования : Accuracy = {acc1:F3}'); + Println($'С MatrixPipeline и масштабированием: Accuracy = {acc2:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/07_Clustering/01_KMeans_SelectK_ByInertia.pas b/InstallerSamples/MachineLearning/07_Clustering/01_KMeans_SelectK_ByInertia.pas new file mode 100644 index 000000000..9d6fadfbc --- /dev/null +++ b/InstallerSamples/MachineLearning/07_Clustering/01_KMeans_SelectK_ByInertia.pas @@ -0,0 +1,41 @@ +// В этом примере подбирается число кластеров для KMeans. +// +// Используются данные MakeBlobs с тремя естественными группами. +// Для каждого значения k выводится inertia: +// чем она меньше, тем ближе объекты к центрам своих кластеров. +// +// Идея метода: +// при увеличении k inertia всегда уменьшается, +// но после "правильного" числа кластеров выигрыш обычно становится небольшим. + +uses MLABC; + +begin + var (X, yTrue) := Datasets.MakeBlobs( + n := 300, + centers := 3, + nFeatures := 2, + clusterStd := 0.8, + centerBox := 6.0, + shuffle := True, + seed := 42 + ); + + Println('Подбор числа кластеров для KMeans'); + Println; + Println('k inertia'); + Println('-' * 18); + + for var k := 2 to 6 do + begin + var model := new KMeans(k, seed := 42); + model.Fit(X); + Println($'{k,-4} {model.Inertia,10:F3}'); + end; + + Println; + Println('Интерпретация результата:'); + Println('- Inertia всегда уменьшается при росте k.'); + Println('- Ищут момент, после которого уменьшение становится не таким заметным.'); + Println('- В этом примере естественно ожидать около 3 кластеров.'); +end. diff --git a/InstallerSamples/MachineLearning/07_Clustering/02_KMeans_vs_DBSCAN.pas b/InstallerSamples/MachineLearning/07_Clustering/02_KMeans_vs_DBSCAN.pas new file mode 100644 index 000000000..8dc9bb6c5 --- /dev/null +++ b/InstallerSamples/MachineLearning/07_Clustering/02_KMeans_vs_DBSCAN.pas @@ -0,0 +1,41 @@ +// В этом примере сравниваются два алгоритма кластеризации: +// KMeans и DBSCAN. +// +// KMeans хорошо работает, когда кластеры компактны и похожи по форме. +// DBSCAN лучше справляется со сложной геометрией кластеров и не требует +// заранее задавать их число. + +uses MLABC; + +begin + var (X, yTrue) := Datasets.MakeMoons( + n := 400, + noise := 0.08, + shuffle := True, + seed := 1 + ); + + var kmeans := new KMeans(2, seed := 42); + kmeans.Fit(X); + var yKMeans := kmeans.PredictLabels(X); + + var dbscan := new DBSCAN(0.22, 5); + dbscan.Fit(X); + var yDBSCAN := dbscan.PredictLabels(X); + + var ariKMeans := ClusteringMetrics.AdjustedRandIndex(yTrue, yKMeans); + var ariDBSCAN := ClusteringMetrics.AdjustedRandIndex(yTrue, yDBSCAN); + + Println('Сравнение KMeans и DBSCAN'); + Println; + Println($'Число кластеров у KMeans: {kmeans.ClustersCount}'); + Println($'Число кластеров у DBSCAN: {dbscan.ClustersCount}'); + Println; + Println($'KMeans: ARI = {ariKMeans:F3}'); + Println($'DBSCAN: ARI = {ariDBSCAN:F3}'); + Println; + Println('Интерпретация результата:'); + Println('- KMeans разбивает данные на компактные области вокруг центров.'); + Println('- DBSCAN умеет находить кластеры сложной формы.'); + Println('- Для двух "лун" DBSCAN обычно лучше отражает реальную структуру данных.'); +end. diff --git a/InstallerSamples/MachineLearning/07_Clustering/03_DBSCAN_SelectEps.pas b/InstallerSamples/MachineLearning/07_Clustering/03_DBSCAN_SelectEps.pas new file mode 100644 index 000000000..26671e814 --- /dev/null +++ b/InstallerSamples/MachineLearning/07_Clustering/03_DBSCAN_SelectEps.pas @@ -0,0 +1,44 @@ +// В этом примере подбирается параметр eps для DBSCAN. +// +// eps задаёт радиус окрестности точки. +// Если eps слишком мал, алгоритм считает много точек шумом. +// Если eps слишком велик, разные группы могут слиться в один кластер. + +uses MLABC; + +begin + var (X, yTrue) := Datasets.MakeMoons( + n := 400, + noise := 0.08, + shuffle := True, + seed := 1 + ); + + Println('Подбор параметра eps для DBSCAN'); + Println; + Println('eps кластеры шум ARI'); + Println('-' * 34); + + foreach var eps in [0.10, 0.14, 0.18, 0.22, 0.26, 0.30] do + begin + var model := new DBSCAN(eps, 5); + model.Fit(X); + + var labels := model.PredictLabels(X); + + var noiseCount := 0; + foreach var clusterLabel in labels do + if clusterLabel = -1 then + noiseCount += 1; + + var ari := ClusteringMetrics.AdjustedRandIndex(yTrue, labels); + + Println($'{eps,4:F2} {model.ClustersCount,10} {noiseCount,6} {ari,8:F3}'); + end; + + Println; + Println('Интерпретация результата:'); + Println('- При слишком маленьком eps шумовых точек становится слишком много.'); + Println('- При слишком большом eps разные группы могут сливаться.'); + Println('- Хороший eps даёт разумное число кластеров и высокое значение ARI.'); +end. diff --git a/InstallerSamples/MachineLearning/07_Clustering/04_RussianCities_KMeans.pas b/InstallerSamples/MachineLearning/07_Clustering/04_RussianCities_KMeans.pas new file mode 100644 index 000000000..2b2ece286 --- /dev/null +++ b/InstallerSamples/MachineLearning/07_Clustering/04_RussianCities_KMeans.pas @@ -0,0 +1,45 @@ +// В этом примере KMeans применяется к реальным данным о российских городах. +// +// Для кластеризации используются два признака: +// log_population и density. +// Это позволяет группировать города не по координатам, +// а по масштабу и плотности. + +uses MLABC; + +begin + var ds := Datasets.RussianCities; + var df := ds.Data; + + df := df.WithColumnFloat('density', row -> row.Float('population') / row.Float('area')); + df := df.WithColumnFloat('log_population', row -> Ln(row.Float('population'))); + + var features := ['log_population', 'density']; + + var pipe := + UDataPipeline.Build( + features, + new StandardScaler, + new KMeans(3, seed := 42) + ); + + var labels := pipe.FitPredict(df); + df.AddIntColumn('cluster', labels, nil); + + Println('Кластеризация российских городов'); + Println; + Println('Признаки: log_population, density'); + Println('Число кластеров: 3'); + + for var cluster := 0 to 2 do + begin + Println; + Println($'Кластер {cluster + 1}:'); + + df.Filter(row -> row.Int('cluster') = cluster) + .SortBy('population', descending := True) + .Select(['city', 'population', 'density']) + .Head(5) + .Print; + end; +end. diff --git a/InstallerSamples/MachineLearning/07_Clustering/05_ClusteringMetrics.exe b/InstallerSamples/MachineLearning/07_Clustering/05_ClusteringMetrics.exe new file mode 100644 index 000000000..cbbb00830 Binary files /dev/null and b/InstallerSamples/MachineLearning/07_Clustering/05_ClusteringMetrics.exe differ diff --git a/InstallerSamples/MachineLearning/07_Clustering/05_ClusteringMetrics.pas b/InstallerSamples/MachineLearning/07_Clustering/05_ClusteringMetrics.pas new file mode 100644 index 000000000..927c2b6b8 --- /dev/null +++ b/InstallerSamples/MachineLearning/07_Clustering/05_ClusteringMetrics.pas @@ -0,0 +1,37 @@ +// Метрики помогают сравнить разные варианты кластеризации. +// +// В этом примере данные естественно делятся на 3 группы. +// Мы сравниваем несколько значений k для KMeans и смотрим, +// как меняются ARI, Silhouette, Calinski-Harabasz и Davies-Bouldin. + +uses MLABC; + +begin + var (X, yTrue) := Datasets.MakeBlobs( + n := 300, + centers := 3, + nFeatures := 2, + clusterStd := 0.8, + centerBox := 6.0, + shuffle := True, + seed := 42 + ); + + Println('Сравнение вариантов кластеризации'); + Println; + Println('k ARI Silhouette Calinski-Harabasz Davies-Bouldin'); + Println('----------------------------------------------------------------'); + + foreach var k in [2, 3, 4, 5] do + begin + var model := new KMeans(k, seed := 42); + var labels := model.FitPredict(X); + + var ari := ClusteringMetrics.AdjustedRandIndex(yTrue, labels); + var sil := ClusteringMetrics.SilhouetteScore(X, labels); + var ch := ClusteringMetrics.CalinskiHarabaszScore(X, labels); + var db := ClusteringMetrics.DaviesBouldinScore(X, labels); + + Println($'{k,1} {ari,5:F3} {sil,5:F3} {ch,8:F1} {db,5:F3}'); + end; +end. diff --git a/InstallerSamples/MachineLearning/08_Datasets/Iris/07_Pipeline.pas b/InstallerSamples/MachineLearning/08_Datasets/Iris/07_Pipeline.pas index 6e688c83f..cf2455a5b 100644 --- a/InstallerSamples/MachineLearning/08_Datasets/Iris/07_Pipeline.pas +++ b/InstallerSamples/MachineLearning/08_Datasets/Iris/07_Pipeline.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; begin var ds := Datasets.Iris; @@ -6,11 +6,11 @@ begin var pipe := DataPipeline.Build( + TaskKind.tkClassification, ds.Target, ds.Features, - new LabelEncoder(ds.Target), // Data preprocessor - new StandardScaler, // Matrix transformer - new LogisticRegression // Model + new StandardScaler, // Matrix transformer + new LogisticRegression // Model ); var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 3); @@ -19,7 +19,7 @@ begin var pred := pipe.Predict(testDf); - var y := testDf.EncodeLabels(ds.Target); + var y := pipe.GetEncodedLabels(testDf); - Println('Accuracy:', Metrics.Accuracy(y, pred):0:3); -end. \ No newline at end of file + Println('Точность:', Metrics.Accuracy(y, pred):0:3); +end. diff --git a/InstallerSamples/MachineLearning/08_Datasets/MoscowHousing/01_Info.pas b/InstallerSamples/MachineLearning/08_Datasets/MoscowHousing/01_Info.pas index 9aa62b750..9f148a1de 100644 --- a/InstallerSamples/MachineLearning/08_Datasets/MoscowHousing/01_Info.pas +++ b/InstallerSamples/MachineLearning/08_Datasets/MoscowHousing/01_Info.pas @@ -9,7 +9,7 @@ begin Println; Println('Первые строки:'); - ds.Head.Println(1); - + ds.Head.Print; + Println; df.Schema.Println end. \ No newline at end of file diff --git a/InstallerSamples/MachineLearning/08_Datasets/RussianCities/01_Info.pas b/InstallerSamples/MachineLearning/08_Datasets/RussianCities/01_Info.pas index 9b3ef3ac9..8f5411a75 100644 --- a/InstallerSamples/MachineLearning/08_Datasets/RussianCities/01_Info.pas +++ b/InstallerSamples/MachineLearning/08_Datasets/RussianCities/01_Info.pas @@ -6,7 +6,7 @@ begin var df := ds.Data; df.Schema.Println; - df.PrintlnInfo; - - df.PrintlnPreview(20); + df.PrintInfo; + Println; + df.Print; end. \ No newline at end of file diff --git a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/01_Info.pas b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/01_Info.pas new file mode 100644 index 000000000..323147968 --- /dev/null +++ b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/01_Info.pas @@ -0,0 +1,14 @@ +uses MLABC; + +begin + Datasets.Language := 'ru'; + + var ds := Datasets.TitanicRu; + ds.Info; + Println; + + ds.ClassCounts.PrintLines(kv -> ds.ClassName(kv.Key) + ' → ' + kv.Value); + Println; + + ds.Data.Print(20); +end. diff --git a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/02_AgeFareScatter.pas b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/02_AgeFareScatter.pas new file mode 100644 index 000000000..96cc90765 --- /dev/null +++ b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/02_AgeFareScatter.pas @@ -0,0 +1,16 @@ +uses MLABC, PlotML; + +begin + var ds := Datasets.TitanicRu; + var df := ds.Data + .Filter(row -> row.IsValid('Возраст') and row.IsValid('ЦенаБилета')); + + var age := df.ToVector('Возраст'); + var fare := df.ToVector('ЦенаБилета'); + var y := df.GetIntColumn('Выжил'); + + Plot.Points(age, fare, y, size := 5); + Plot.XLabel('Возраст'); + Plot.YLabel('Цена билета'); + Plot.Title := 'Титаник: возраст, цена билета и выживание'; +end. diff --git a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/03_Preprocessing.pas b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/03_Preprocessing.pas new file mode 100644 index 000000000..a5b9d32ef --- /dev/null +++ b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/03_Preprocessing.pas @@ -0,0 +1,24 @@ +uses MLABC; + +begin + var ds := Datasets.TitanicRu; + var df := ds.Data; + + Println('До предобработки:'); + df.Select(['Id', 'Имя', 'Выжил', 'Класс', 'Пол', 'Возраст', 'ПортПосадки']).Head(10).Print; + Println; + + // Убираем служебный номер и длинное текстовое поле. + df := df.Drop(['Id', 'Имя']); + + // Заполняем пропуски в возрасте средним значением. + var ageImputer := new Imputer(['Возраст']); + df := ageImputer.FitTransform(df); + + // Заполняем пропуски в порту посадки константой. + var portImputer := new Imputer('Саутгемптон', ['ПортПосадки']); + df := portImputer.FitTransform(df); + + Println('После предобработки:'); + df.Select(['Выжил', 'Класс', 'Пол', 'Возраст', 'ПортПосадки']).Head(10).Print; +end. diff --git a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/04_AgeHistogramBySurvival.pas b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/04_AgeHistogramBySurvival.pas new file mode 100644 index 000000000..73d2a0e54 --- /dev/null +++ b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/04_AgeHistogramBySurvival.pas @@ -0,0 +1,23 @@ +uses MLABC, PlotML; + +begin + var ds := Datasets.TitanicRu; + var df := ds.Data.Filter(row -> row.IsValid('Возраст')); + + var diedAges: array of real := df + .Filter(row -> row.Int('Выжил') = 0) + .ToVector('Возраст').Data; + + var survivedAges: array of real := df + .Filter(row -> row.Int('Выжил') = 1) + .ToVector('Возраст').Data; + + Println($'Пассажиров с известным возрастом: {df.RowCount}'); + Println($'Не выжили: {diedAges.Length}'); + Println($'Выжили: {survivedAges.Length}'); + + Plot.HistMany([diedAges,survivedAges], bins := 20, colors := [Colors.IndianRed, Colors.SteelBlue], alpha := 0.45, legend := ['не выжили','выжили']); + Plot.Title := 'Титаник: возраст выживших (голубой) и невыживших (красный)'; + Plot.XLabel('Возраст'); + Plot.YLabel('Число пассажиров'); +end. diff --git a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/05_Classification.pas b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/05_Classification.pas new file mode 100644 index 000000000..a157130a7 --- /dev/null +++ b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/05_Classification.pas @@ -0,0 +1,32 @@ +uses MLABC; + +begin + var ds := Datasets.TitanicRu; + var features := ['Класс', 'Пол', 'Возраст', 'БратьяИСупруги', 'РодителиИДети', 'ЦенаБилета', 'ПортПосадки']; + + var df := ds.Data.Drop(['Id', 'Имя']); + + var (trainDf, testDf) := + df.StratifiedTrainTestSplit(ds.Target, testRatio := 0.2, seed := 42); + + var pipe := + DataPipeline.Build( + TaskKind.tkClassification, + ds.Target, + features, + new Imputer(['Возраст']), + new Imputer('Саутгемптон', ['ПортПосадки']), + new OneHotEncoder('Пол'), + new OneHotEncoder('ПортПосадки'), + new StandardScaler, + new LogisticRegression(learningRate := 0.1, epochs := 2000) + ); + + pipe.Fit(trainDf); + + var pred := pipe.Predict(testDf); + var y := pipe.GetEncodedLabels(testDf); + + Println('Классификация выживания на Титанике (логистическая регрессия)'); + Println($'Accuracy = {Metrics.Accuracy(y, pred):F3}'); +end. diff --git a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/06_ModelComparison.pas b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/06_ModelComparison.pas new file mode 100644 index 000000000..f0e024d27 --- /dev/null +++ b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/06_ModelComparison.pas @@ -0,0 +1,48 @@ +uses MLABC; + +begin + var ds := Datasets.TitanicRu; + var df := ds.Data.Drop(['Id', 'Имя']); + + // Заполняем пропуски. + var ageImputer := new Imputer(['Возраст']); + df := ageImputer.FitTransform(df); + + var portImputer := new Imputer('Саутгемптон', ['ПортПосадки']); + df := portImputer.FitTransform(df); + + // Кодируем категориальные признаки числами. + var sexEncoder := new LabelEncoder('Пол'); + df := sexEncoder.FitTransform(df); + + var portEncoder := new LabelEncoder('ПортПосадки'); + df := portEncoder.FitTransform(df); + + var features := ['Класс', 'Пол', 'Возраст', 'БратьяИСупруги', 'РодителиИДети', 'ЦенаБилета', 'ПортПосадки']; + var X := df.ToMatrix(features); + var y := df.GetIntColumn('Выжил'); + + var (Xtrain, Xtest, ytrain, ytest) := Validation.TrainTestSplit(X, y, testRatio := 0.2, seed := 42); + + var scaler := new StandardScaler; + scaler.Fit(Xtrain); + var XtrainScaled := scaler.Transform(Xtrain); + var XtestScaled := scaler.Transform(Xtest); + + var lr := new LogisticRegression(learningRate := 0.01, epochs := 2000); + lr.Fit(XtrainScaled, ytrain); + var predLR := lr.Predict(XtestScaled); + + var tree := new DecisionTreeClassifier(maxDepth := 5, minSamplesLeaf := 3, minSamplesSplit := 6); + tree.Fit(Xtrain, ytrain); + var predTree := tree.Predict(Xtest); + + var forest := new RandomForestClassifier(nTrees := 100, maxDepth := 6, minSamplesLeaf := 3, minSamplesSplit := 6); + forest.Fit(Xtrain, ytrain); + var predForest := forest.Predict(Xtest); + + Println('Сравнение моделей на TitanicRu'); + Println($'LogisticRegression: Accuracy = {Metrics.Accuracy(ytest, predLR):F3}'); + Println($'DecisionTreeClassifier: Accuracy = {Metrics.Accuracy(ytest, predTree):F3}'); + Println($'RandomForestClassifier: Accuracy = {Metrics.Accuracy(ytest, predForest):F3}'); +end. diff --git a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/07_PredictProba.pas b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/07_PredictProba.pas new file mode 100644 index 000000000..e225ea479 --- /dev/null +++ b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/07_PredictProba.pas @@ -0,0 +1,39 @@ +uses MLABC; + +begin + var ds := Datasets.TitanicRu; + var features := ['Класс', 'Пол', 'Возраст', 'БратьяИСупруги', 'РодителиИДети', 'ЦенаБилета', 'ПортПосадки']; + + var df := ds.Data.Drop(['Id', 'Имя']); + + var (trainDf, testDf) := + df.StratifiedTrainTestSplit(ds.Target, testRatio := 0.2, seed := 42); + + var pipe := + DataPipeline.Build( + TaskKind.tkClassification, + 'Выжил', + features, + new Imputer(['Возраст']), + new Imputer('Саутгемптон', ['ПортПосадки']), + new OneHotEncoder('Пол'), + new OneHotEncoder('ПортПосадки'), + new StandardScaler, + new LogisticRegression(learningRate := 0.01, epochs := 2000) + ); + + pipe.Fit(trainDf); + + var pred := pipe.Predict(testDf); + var y := pipe.GetEncodedLabels(testDf); + var proba := pipe.PredictProba(testDf); + var classes := pipe.GetClassLabels; + + Println('TitanicRu: вероятности классов'); + Println($'Accuracy = {Metrics.Accuracy(y, pred):F3}'); + Println; + Println('Вероятности для первого пассажира тестовой выборки:'); + + for var j := 0 to classes.Length - 1 do + Println($' {classes[j]}: {proba[0, j]:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/08_CrossValidation.pas b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/08_CrossValidation.pas new file mode 100644 index 000000000..99aa63799 --- /dev/null +++ b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/08_CrossValidation.pas @@ -0,0 +1,58 @@ +uses MLABC; + +function BuildPipe(features: array of string): DataPipeline; +begin + Result := + DataPipeline.Build( + TaskKind.tkClassification, + 'Выжил', + features, + new Imputer(['Возраст']), + new Imputer('Саутгемптон', ['ПортПосадки']), + new OneHotEncoder('Пол'), + new OneHotEncoder('ПортПосадки'), + new StandardScaler, + new RandomForestClassifier(nTrees := 100, maxDepth := 6, minSamplesLeaf := 3, minSamplesSplit := 6) + ); +end; + +begin + var ds := Datasets.TitanicRu; + var features := ['Класс', 'Пол', 'Возраст', 'БратьяИСупруги', 'РодителиИДети', 'ЦенаБилета', 'ПортПосадки']; + var df := ds.Data.Drop(['Id', 'Имя']); + + var (trainDf, testDf) := + df.StratifiedTrainTestSplit(ds.Target, testRatio := 0.2, seed := 42); + + var pipe := BuildPipe(features); + pipe.Fit(trainDf); + + var pred := pipe.Predict(testDf); + var y := pipe.GetEncodedLabels(testDf); + var testAccuracy := Metrics.Accuracy(y, pred); + + var total := 0.0; + var folds := Validation.StratifiedKFold(df.GetIntColumn('Выжил'), 5, seed := 1); + var foldsCount := 0; + + foreach var (trainIdx, testIdx) in folds do + begin + var foldTrain := df.TakeRows(trainIdx); + var foldTest := df.TakeRows(testIdx); + + var foldPipe := BuildPipe(features); + foldPipe.Fit(foldTrain); + + var foldPred := foldPipe.Predict(foldTest); + var foldY := foldPipe.GetEncodedLabels(foldTest); + + total += Metrics.Accuracy(foldY, foldPred); + foldsCount += 1; + end; + + var cvAccuracy := total / foldsCount; + + Println('Оценка RandomForestClassifier двумя способами'); + Println($'Accuracy на тестовой выборке: {testAccuracy:F3}'); + Println($'Средняя Accuracy по кросс-валидации: {cvAccuracy:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/09_FeatureImportance.pas b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/09_FeatureImportance.pas new file mode 100644 index 000000000..3f39e4992 --- /dev/null +++ b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/09_FeatureImportance.pas @@ -0,0 +1,31 @@ +uses MLABC; + +begin + var ds := Datasets.TitanicRu; + var df := ds.Data.Drop(['Id', 'Имя']); + + var ageImputer := new Imputer(['Возраст']); + df := ageImputer.FitTransform(df); + + var portImputer := new Imputer('Саутгемптон', ['ПортПосадки']); + df := portImputer.FitTransform(df); + + var sexEncoder := new LabelEncoder('Пол'); + df := sexEncoder.FitTransform(df); + + var portEncoder := new LabelEncoder('ПортПосадки'); + df := portEncoder.FitTransform(df); + + var features := ['Класс', 'Пол', 'Возраст', 'БратьяИСупруги', 'РодителиИДети', 'ЦенаБилета', 'ПортПосадки']; + var X := df.ToMatrix(features); + var y := df.GetIntColumn('Выжил'); + + var model := new RandomForestClassifier(nTrees := 100, maxDepth := 6, minSamplesLeaf := 3, minSamplesSplit := 6, seed := 42); + model.Fit(X, y); + + var imp := model.FeatureImportances; + + Println('Важность признаков для RandomForestClassifier'); + for var i := 0 to features.Length - 1 do + Println($'{features[i],-18}: {imp[i]:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/10_SurvivalBySexAndClass.pas b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/10_SurvivalBySexAndClass.pas new file mode 100644 index 000000000..6f6061c9f --- /dev/null +++ b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/10_SurvivalBySexAndClass.pas @@ -0,0 +1,26 @@ +uses MLABC; + +function SurvivalRate(df: DataFrame): real; +begin + Result := df.GetIntColumn('Выжил').Average; +end; + +begin + var ds := Datasets.TitanicRu; + var df := ds.Data; + + Println('Доля выживших по полу:'); + foreach var sex in |'муж', 'жен'| do + begin + var sub := df.Filter(r -> r.Str('Пол') = sex); + Println($' {sex,-4}: {100 * SurvivalRate(sub):F1}%'); + end; + + Println; + Println('Доля выживших по классу:'); + for var cls := 1 to 3 do + begin + var sub := df.Filter(r -> r.Int('Класс') = cls); + Println($' класс {cls}: {100 * SurvivalRate(sub):F1}%'); + end; +end. diff --git a/InstallerSamples/MachineLearning/08_Datasets/_Synthetic/05_MakeSpiral.pas b/InstallerSamples/MachineLearning/08_Datasets/_Synthetic/05_MakeSpiral.pas index b10004b32..ec70706e5 100644 --- a/InstallerSamples/MachineLearning/08_Datasets/_Synthetic/05_MakeSpiral.pas +++ b/InstallerSamples/MachineLearning/08_Datasets/_Synthetic/05_MakeSpiral.pas @@ -11,6 +11,6 @@ begin seed := 1 ); - Plot.Title('MakeSpiral синтетический датасет'); + Plot.Title := 'MakeSpiral синтетический датасет'; Plot.Points(X.Col(0), X.Col(1), LabelsToInts(y), size := 5); end. \ No newline at end of file diff --git a/InstallerSamples/MachineLearning/09_Visualization/01_Scatter_Iris.pas b/InstallerSamples/MachineLearning/09_Visualization/01_Scatter_Iris.pas new file mode 100644 index 000000000..76e41786d --- /dev/null +++ b/InstallerSamples/MachineLearning/09_Visualization/01_Scatter_Iris.pas @@ -0,0 +1,28 @@ +// В этом примере строится scatter plot для двух признаков датасета Iris. +// +// По оси X откладывается длина чашелистика, +// по оси Y — ширина чашелистика. +// +// Цвет точки показывает класс цветка. + +uses MLABC, PlotML; + +begin + var ds := Datasets.Iris; + var df := ds.Data; + + // Берем два признака для двумерного графика. + var sepalLength := df.ToVector('sepal_length'); + var sepalWidth := df.ToVector('sepal_width'); + + // Кодируем классы в числа, чтобы раскрасить точки по видам Iris. + var y := df.EncodeLabels(ds.Target); + + Println('Scatter plot для датасета Iris'); + Println; + Println('По оси X: sepal_length'); + Println('По оси Y: sepal_width'); + + Plot.Points(sepalLength, sepalWidth, y, size := 6); + Plot.SetLabels('Iris: scatter plot', 'sepal_length', 'sepal_width'); +end. diff --git a/InstallerSamples/MachineLearning/09_Visualization/02_PairPlot_Iris.pas b/InstallerSamples/MachineLearning/09_Visualization/02_PairPlot_Iris.pas new file mode 100644 index 000000000..d09018688 --- /dev/null +++ b/InstallerSamples/MachineLearning/09_Visualization/02_PairPlot_Iris.pas @@ -0,0 +1,26 @@ +// В этом примере строится pair plot для всех признаков датасета Iris. +// +// На диагонали расположены гистограммы отдельных признаков, +// а вне диагонали — scatter plot для всех пар признаков. +// +// Цвет точки показывает класс цветка. + +uses MLABC, PlotML; + +begin + var ds := Datasets.Iris; + var df := ds.Data; + + // Преобразуем признаки в матрицу. + var X := df.ToMatrix(ds.Features); + + // Кодируем классы в числа, чтобы раскрасить точки по видам Iris. + var y := df.EncodeLabels(ds.Target); + + Println('Pair plot для датасета Iris'); + Println; + Println('На диагонали показаны гистограммы признаков.'); + Println('Вне диагонали показаны scatter plot для всех пар признаков.'); + + Plot.PairPlot(X, LabelsToInts(y), ds.Features); +end. diff --git a/InstallerSamples/MachineLearning/09_Visualization/03_Histogram_Iris.pas b/InstallerSamples/MachineLearning/09_Visualization/03_Histogram_Iris.pas new file mode 100644 index 000000000..dc0999d07 --- /dev/null +++ b/InstallerSamples/MachineLearning/09_Visualization/03_Histogram_Iris.pas @@ -0,0 +1,21 @@ +// В этом примере строится гистограмма одного признака датасета Iris. +// +// Гистограмма показывает, как распределены значения признака +// и в каких диапазонах они встречаются чаще. + +uses MLABC, PlotML; + +begin + var ds := Datasets.Iris; + var df := ds.Data; + + // Берем длину лепестка. + var petalLength := df.ToVector('petal_length'); + + Println('Гистограмма признака petal_length'); + Println; + Println('Гистограмма помогает увидеть распределение значений признака.'); + + Plot.Hist(petalLength, bins := 12); + Plot.SetLabels('Iris: histogram', 'petal_length', 'count'); +end. diff --git a/InstallerSamples/MachineLearning/09_Visualization/04_Heatmap_Correlation.pas b/InstallerSamples/MachineLearning/09_Visualization/04_Heatmap_Correlation.pas new file mode 100644 index 000000000..fc2ce8641 --- /dev/null +++ b/InstallerSamples/MachineLearning/09_Visualization/04_Heatmap_Correlation.pas @@ -0,0 +1,25 @@ +// В этом примере строится heatmap матрицы корреляций +// для числовых признаков датасета Iris. +// +// Тепловая карта помогает быстро увидеть, +// какие признаки связаны друг с другом сильнее. + +uses MLABC, PlotML; + +begin + var ds := Datasets.Iris; + var df := ds.Data; + + // Вычисляем матрицу корреляций для числовых столбцов. + var corrDf := Statistics.CorrelationMatrix(df); + var corrNames := corrDf.Schema.ColumnNames.Skip(1).ToArray; + var corr := corrDf.ToMatrix(corrNames); + + Println('Heatmap матрицы корреляций для Iris'); + Println; + Println('В строках и столбцах расположены признаки.'); + Println('Внутри каждой клетки показан коэффициент корреляции.'); + + Plot.Heatmap(corr, corrNames); + Plot.Title := 'Iris: correlation heatmap'; +end. diff --git a/InstallerSamples/MachineLearning/09_Visualization/05_Surface_ClassificationBoundary.pas b/InstallerSamples/MachineLearning/09_Visualization/05_Surface_ClassificationBoundary.pas new file mode 100644 index 000000000..a22d6b54a --- /dev/null +++ b/InstallerSamples/MachineLearning/09_Visualization/05_Surface_ClassificationBoundary.pas @@ -0,0 +1,29 @@ +// В этом примере Surface показывает, +// как модель делит плоскость на области классов. + +uses MLABC, PlotML; + +begin + var (X, y) := Datasets.MakeMoons( + n := 300, + noise := 0.18, + seed := 42 + ); + + var model := new DecisionTreeClassifier(maxDepth := 5, minSamplesSplit := 6, minSamplesLeaf := 3, seed := 42); + model.Fit(X, y); + var acc := ClassificationMetrics.Accuracy(y, model.Predict(X)); + + var x1 := X.Col(0); + var x2 := X.Col(1); + var labels := LabelsToInts(y); + + Println('Граница решений для DecisionTreeClassifier'); + Println; + Println('Цветной фон показывает области, которые модель относит к разным классам.'); + Println($'Accuracy = {acc:F3}'); + + Plot.Surface(x1, x2, 80, 80, G -> model.PredictLabels(G), Palettes.Pastel); + Plot.Points(x1, x2, labels, size := 6); + Plot.Title := $'DecisionTreeClassifier: decision boundary (Acc = {acc:F3})'; +end. diff --git a/InstallerSamples/MachineLearning/09_Visualization/06_Surface_LogisticVsTree.pas b/InstallerSamples/MachineLearning/09_Visualization/06_Surface_LogisticVsTree.pas new file mode 100644 index 000000000..3e43b17bf --- /dev/null +++ b/InstallerSamples/MachineLearning/09_Visualization/06_Surface_LogisticVsTree.pas @@ -0,0 +1,34 @@ +// В этом примере сравниваются границы решений +// логистической регрессии и дерева решений. + +uses MLABC, PlotML; + +begin + var (X, y) := Datasets.MakeMoons( + n := 300, + noise := 0.18, + seed := 42 + ); + + var logreg := new LogisticRegression(learningRate := 0.05, epochs := 1000); + logreg.Fit(X, y); + var accLR := ClassificationMetrics.Accuracy(y, logreg.Predict(X)); + + var tree := new DecisionTreeClassifier(maxDepth := 5, minSamplesSplit := 6, minSamplesLeaf := 3, seed := 42); + tree.Fit(X, y); + var accTree := ClassificationMetrics.Accuracy(y, tree.Predict(X)); + + var x1 := X.Col(0); + var x2 := X.Col(1); + var labels := LabelsToInts(y); + + var fig := Plot.Grid(1, 2); + + fig[0,0].Surface(x1, x2, 80, 80, G -> logreg.PredictLabels(G), Palettes.Pastel); + fig[0,0].Points(x1, x2, labels, size := 6); + fig[0,0].Title := $'LogisticRegression (Acc = {accLR:F3})'; + + fig[0,1].Surface(x1, x2, 80, 80, G -> tree.PredictLabels(G), Palettes.Pastel); + fig[0,1].Points(x1, x2, labels, size := 6); + fig[0,1].Title := $'DecisionTreeClassifier (Acc = {accTree:F3})'; +end. diff --git a/InstallerSamples/MachineLearning/09_Visualization/07_Surface_TreeVsForest.pas b/InstallerSamples/MachineLearning/09_Visualization/07_Surface_TreeVsForest.pas new file mode 100644 index 000000000..8fbf6be96 --- /dev/null +++ b/InstallerSamples/MachineLearning/09_Visualization/07_Surface_TreeVsForest.pas @@ -0,0 +1,37 @@ +// В этом примере сравниваются границы решений +// дерева решений и случайного леса. + +uses MLABC, PlotML; + +begin + var (X, y) := Datasets.MakeCircles( + n := 300, + noise := 0.3, + factor := 0.5, + flipProb := 0.08, + scale := 3.0, + seed := 42 + ); + + var tree := new DecisionTreeClassifier(maxDepth := 6, minSamplesSplit := 6, minSamplesLeaf := 3, seed := 42); + tree.Fit(X, y); + var accTree := ClassificationMetrics.Accuracy(y, tree.Predict(X)); + + var forest := new RandomForestClassifier(nTrees := 100, maxDepth := 6, minSamplesSplit := 6, minSamplesLeaf := 3, seed := 42); + forest.Fit(X, y); + var accForest := ClassificationMetrics.Accuracy(y, forest.Predict(X)); + + var x1 := X.Col(0); + var x2 := X.Col(1); + var labels := LabelsToInts(y); + + var fig := Plot.Grid(1, 2); + + fig[0,0].Surface(x1, x2, 80, 80, G -> tree.PredictLabels(G), Palettes.Pastel); + fig[0,0].Points(x1, x2, labels, size := 6); + fig[0,0].Title := $'DecisionTree (Acc = {accTree:F3})'; + + fig[0,1].Surface(x1, x2, 80, 80, G -> forest.PredictLabels(G), Palettes.Pastel); + fig[0,1].Points(x1, x2, labels, size := 6); + fig[0,1].Title := $'RandomForest (Acc = {accForest:F3})'; +end. diff --git a/InstallerSamples/MachineLearning/10_RealTasks/01_Iris_EndToEnd_Pipeline.exe b/InstallerSamples/MachineLearning/10_RealTasks/01_Iris_EndToEnd_Pipeline.exe new file mode 100644 index 000000000..2cd94563d Binary files /dev/null and b/InstallerSamples/MachineLearning/10_RealTasks/01_Iris_EndToEnd_Pipeline.exe differ diff --git a/InstallerSamples/MachineLearning/10_RealTasks/01_Iris_EndToEnd_Pipeline.pas b/InstallerSamples/MachineLearning/10_RealTasks/01_Iris_EndToEnd_Pipeline.pas new file mode 100644 index 000000000..f45b4ebe8 --- /dev/null +++ b/InstallerSamples/MachineLearning/10_RealTasks/01_Iris_EndToEnd_Pipeline.pas @@ -0,0 +1,28 @@ +// Полный пример задачи классификации: +// загружаем датасет, делим его на выборки, +// обучаем pipeline и оцениваем качество. + +uses MLABC; + +begin + var ds := Datasets.Iris; + var (trainDs, testDs) := ds.StratifiedTrainTestSplit(testRatio := 0.3, seed := 42); + + var pipe := DataPipeline.Build( + TaskKind.tkClassification, + ds.Target, + ds.Features, + new StandardScaler, + new LogisticRegression(learningRate := 0.05, epochs := 1000) + ); + + pipe.Fit(trainDs.Data); + + var pred := pipe.Predict(testDs.Data); + var yTest := pipe.GetEncodedLabels(testDs.Data); + var acc := ClassificationMetrics.Accuracy(yTest, pred); + + Println('Классификация Iris: полный пример'); + Println; + Println($'Accuracy = {acc:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/10_RealTasks/02_MoscowHousing_EndToEnd_Pipeline.exe b/InstallerSamples/MachineLearning/10_RealTasks/02_MoscowHousing_EndToEnd_Pipeline.exe new file mode 100644 index 000000000..47c74c137 Binary files /dev/null and b/InstallerSamples/MachineLearning/10_RealTasks/02_MoscowHousing_EndToEnd_Pipeline.exe differ diff --git a/InstallerSamples/MachineLearning/10_RealTasks/02_MoscowHousing_Pipeline.pas b/InstallerSamples/MachineLearning/10_RealTasks/02_MoscowHousing_Pipeline.pas new file mode 100644 index 000000000..ac7c05a0f --- /dev/null +++ b/InstallerSamples/MachineLearning/10_RealTasks/02_MoscowHousing_Pipeline.pas @@ -0,0 +1,34 @@ +// Полный пример задачи регрессии: +// загружаем датасет, делим его на выборки, +// обучаем pipeline и оцениваем качество. + +uses MLABC; + +begin + var ds := Datasets.MoscowHousing; + var df := ds.Data; + + var features := ['rooms', 'area', 'kitchen_area', 'floor', 'floors_total', 'metro_minutes', 'renovation']; + var target := 'price'; + + var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 42); + + var pipe := DataPipeline.Build( + TaskKind.tkRegression, + target, + features, + new OneHotEncoder('renovation'), + new StandardScaler, + new LinearRegression + ); + + pipe.Fit(trainDf); + + var pred := pipe.Predict(testDf); + var yTest := testDf.ToVector(target); + var r2 := Metrics.R2(yTest, pred); + + Println('Прогноз цен на жильё: полный пример'); + Println; + Println($'R² = {r2:F3}'); +end. diff --git a/InstallerSamples/MachineLearning/13_Seminars/seminar1.pas b/InstallerSamples/MachineLearning/13_Seminars/seminar1.pas index 8fec04bab..5ea75164c 100644 --- a/InstallerSamples/MachineLearning/13_Seminars/seminar1.pas +++ b/InstallerSamples/MachineLearning/13_Seminars/seminar1.pas @@ -7,7 +7,7 @@ begin df.Schema.Println; // Заполняем пропуски в числовых столбцах средним значением - var imputer := new Imputer('population', 'lat', 'lon'); + var imputer := new Imputer(['population', 'lat', 'lon']); df := imputer.FitTransform(df); // Кодируем категориальные признаки @@ -17,5 +17,5 @@ begin var encoder2 := new LabelEncoder('federal_district'); df := encoder2.FitTransform(df); - df.PrintPreview(6); -end. \ No newline at end of file + df.Print; +end. diff --git a/InstallerSamples/MachineLearning/13_Seminars/seminar2.pas b/InstallerSamples/MachineLearning/13_Seminars/seminar2.pas index c34be1d11..3ca94a083 100644 --- a/InstallerSamples/MachineLearning/13_Seminars/seminar2.pas +++ b/InstallerSamples/MachineLearning/13_Seminars/seminar2.pas @@ -13,7 +13,7 @@ begin ]); // Препроцессинг - var imputer := new Imputer('population', 'lat', 'lon'); + var imputer := new Imputer(['population', 'lat', 'lon']); df := imputer.FitTransform(df); var le1 := new LabelEncoder('region_name'); @@ -35,4 +35,4 @@ begin var preds := model.Predict(X); Println('RMSE:', Metrics.RMSE(y, preds):0:3); -end. \ No newline at end of file +end. diff --git a/InstallerSamples/MachineLearning/13_Seminars/seminar3.pas b/InstallerSamples/MachineLearning/13_Seminars/seminar3.pas index be8f0e523..94d701724 100644 --- a/InstallerSamples/MachineLearning/13_Seminars/seminar3.pas +++ b/InstallerSamples/MachineLearning/13_Seminars/seminar3.pas @@ -4,7 +4,7 @@ begin var df := CsvLoader.Load('towns_russia.csv', inferCategorical := true); df := df.Select(['population', 'lat', 'lon', 'region_name', 'federal_district']); - var imputer := new Imputer('population', 'lat', 'lon'); + var imputer := new Imputer(['population', 'lat', 'lon']); df := imputer.FitTransform(df); var le1 := new LabelEncoder('region_name'); @@ -28,4 +28,4 @@ begin var preds := model.Predict(Xscaled); Println('RMSE:', Metrics.RMSE(y, preds):0:3); -end. \ No newline at end of file +end. diff --git a/InstallerSamples/MachineLearning/13_Seminars/seminar4.pas b/InstallerSamples/MachineLearning/13_Seminars/seminar4.pas index b4822961b..4995eb64b 100644 --- a/InstallerSamples/MachineLearning/13_Seminars/seminar4.pas +++ b/InstallerSamples/MachineLearning/13_Seminars/seminar4.pas @@ -8,10 +8,11 @@ begin var target := 'price'; - var (trainDf, testDf) := df.TrainTestSplit(0.2, 42); + var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 42); var pipe := DataPipeline.Build( // сборка pipeline: target + features + шаги + TaskKind.tkRegression, target, // целевая переменная features, // список признаков new OneHotEncoder('renovation'), // DataFrame-уровень: кодирование категории diff --git a/InstallerSamples/MachineLearning/13_Seminars/seminar5_synthetic.pas b/InstallerSamples/MachineLearning/13_Seminars/seminar5_synthetic.pas new file mode 100644 index 000000000..d5b4cba7d --- /dev/null +++ b/InstallerSamples/MachineLearning/13_Seminars/seminar5_synthetic.pas @@ -0,0 +1,122 @@ +uses MLABC, PlotML; + +function ToIntArray(v: Vector): array of integer; +begin + Result := v.Data.Select(t -> integer(t)).ToArray; +end; + +function BinLabels(v: Vector; bins: integer := 8): array of integer; +begin + Result := new integer[v.Length]; + + var vmin := v.Min; + var vmax := v.Max; + + if vmax = vmin then + exit; + + var w := (vmax - vmin) / bins; + + for var i := 0 to v.Length - 1 do + begin + var k := trunc((v[i] - vmin) / w); + if k >= bins then k := bins - 1; + if k < 0 then k := 0; + Result[i] := k; + end; +end; + +procedure DrawDataset(cell: Cell; X: Matrix; labels: array of integer; title: string); +begin + var x1 := X.Col(0); + var x2 := X.Col(1); + + // защита от рассинхронизации + if labels.Length <> X.RowCount then + raise new Exception('Labels length mismatch'); + + cell.SetPalette(Palettes.Bright); + cell.Points(x1, x2, labels, size := 5); + cell.Title := title; +end; + +begin + var fig := Plot.Grid(2, 3); + + // --- Blobs + var (X1, y1) := Datasets.MakeBlobs( + n := 300, + centers := 3, + nFeatures := 2, + clusterStd := 0.7, + clusterStdVar := 0.4, + centerBox := 5.0, + classBalance := 1.0, + noisePoints := 20, + shuffle := True, + seed := 1 + ); + DrawDataset(fig[0,0], X1, ToIntArray(y1), 'MakeBlobs'); + + // --- Moons + var (X2, y2) := Datasets.MakeMoons( + n := 300, + noise := 0.1, + shuffle := True, + seed := 2 + ); + DrawDataset(fig[0,1], X2, ToIntArray(y2), 'MakeMoons'); + + // --- Circles + var (X3, y3) := Datasets.MakeCircles( + n := 300, + noise := 0.08, + factor := 0.45, + classBalance := 0.5, + flipProb := 0.0, + scale := 3.0, + shuffle := True, + seed := 3 + ); + DrawDataset(fig[0,2], X3, ToIntArray(y3), 'MakeCircles'); + + // --- Spiral + var (X4, y4) := Datasets.MakeSpiral( + n := 300, + noise := 0.03, + turns := 2.5, + shuffle := True, + seed := 4 + ); + DrawDataset(fig[1,0], X4, ToIntArray(y4), 'MakeSpiral'); + + // --- Regression (биннинг) + var (X5, y5) := Datasets.MakeRegression( + n := 300, + nFeatures := 2, + nInformative := 2, + noise := 0.02, + coefScale := 1.0, + bias := 0.0, + nonlinearStrength := 3.0, + shuffle := True, + seed := 5 + ); + var labels := ArrFill(X5.RowCount, 0); + DrawDataset(fig[1,1], X5, labels, 'MakeRegression'); + + // --- Classification + var (X6, y6) := Datasets.MakeClassification( + n := 300, + nFeatures := 2, + nInformative := 2, + nRedundant := 0, + noise := 0.2, + classSep := 2.5, + flipProb := 0.05, + classBalance := 0.5, + shuffle := True, + seed := 6 + ); + DrawDataset(fig[1,2], X6, ToIntArray(y6), 'MakeClassification'); +end. \ No newline at end of file diff --git a/InstallerSamples/WhatsNew/3_11_1/DataFrameABC1.pas b/InstallerSamples/WhatsNew/3_11_1/DataFrameABC1.pas index ad4257d69..364429ddc 100644 --- a/InstallerSamples/WhatsNew/3_11_1/DataFrameABC1.pas +++ b/InstallerSamples/WhatsNew/3_11_1/DataFrameABC1.pas @@ -32,10 +32,10 @@ begin Kat,21,NA '''); - df.Filter(row -> row.Int('age') > 20).Println; - - df.GroupBy('age').Mean('score').Println; - + df.Filter(row -> row.Int('age') > 20).Print; + Println; + df.GroupBy('age').Mean('score').Print; + Println; var stat := df.Describe('score'); Println($'score: count={stat.Count}, min={stat.Min}, max={stat.Max}, mean={stat.Mean}, std={stat.Std.Round(3)}'); end. \ No newline at end of file diff --git a/Libraries/WeifenLuo.WinFormsUI.Docking.dll b/Libraries/WeifenLuo.WinFormsUI.Docking.dll index c979b73e1..3d620c6fe 100644 Binary files a/Libraries/WeifenLuo.WinFormsUI.Docking.dll and b/Libraries/WeifenLuo.WinFormsUI.Docking.dll differ diff --git a/Libraries/WeifenLuo/WinFormsUI/Docking/VS2005DockPaneStrip.cs b/Libraries/WeifenLuo/WinFormsUI/Docking/VS2005DockPaneStrip.cs index 7f85a20f4..5ab06be44 100644 --- a/Libraries/WeifenLuo/WinFormsUI/Docking/VS2005DockPaneStrip.cs +++ b/Libraries/WeifenLuo/WinFormsUI/Docking/VS2005DockPaneStrip.cs @@ -108,7 +108,7 @@ namespace WeifenLuo.WinFormsUI.Docking private const int _DocumentStripGapTop = -1; private const int _DocumentStripGapBottom = 1; - private const int _DocumentTabMaxWidth = 500; + private const int _DocumentTabMaxWidth = 600; private const int _DocumentButtonGapTop = 4; private const int _DocumentButtonGapBottom = 4; private const int _DocumentButtonGapBetween = 0; diff --git a/Release/pabcversion.txt b/Release/pabcversion.txt index bfb3f8943..7b124d262 100644 --- a/Release/pabcversion.txt +++ b/Release/pabcversion.txt @@ -1 +1 @@ -3.11.1.3812 +3.11.1.3814 diff --git a/ReleaseGenerators/Files/Datasets/moscow_housing.csv b/ReleaseGenerators/Files/Datasets/moscow_housing.csv new file mode 100644 index 000000000..a663a907a --- /dev/null +++ b/ReleaseGenerators/Files/Datasets/moscow_housing.csv @@ -0,0 +1,1501 @@ +price,metro_minutes,rooms,area,kitchen_area,floor,floors_total,renovation +108800000,5,3,108.8,15.6,3,3,without +53000000,9,3,96.9,11.1,2,5,without +47000000,9,3,103.8,15,6,6,without +170000000,10,3,117,6,3,5,euro +52500000,11,3,92,20,4,21,cosmetic +22450000,7,3,83.5,12.5,16,22,without +17500000,10,3,77.1,13.5,22,25,without +41500000,5,3,106.7,30,15,17,cosmetic +46000000,28,3,70,12.4,2,14,without +33999000,4,3,95.5,14.5,29,29,without +100000000,5,3,90.53,14.1,10,21,euro +24500000,8,3,68.5,9.1,4,6,cosmetic +56650000,17,3,82.2,7.6,37,41,without +115732500,6,3,118.7,16.5,3,5,without +99600000,9,3,83,10,7,13,euro +44000000,17,3,87,13.8,15,22,without +170000000,10,3,169,20.6,4,6,without +131000000,5,3,114,16.1,7,21,euro +40500000,10,3,106.1,32.4,48,58,without +43500000,10,3,105.6,15.6,11,53,without +120000000,15,3,100,14.9,6,7,euro +144170000,5,3,111,15.8,6,8,cosmetic +148070000,7,3,113.9,17,6,12,without +27300000,14,3,90,13,3,9,without +33800000,7,3,90.8,12.6,2,30,cosmetic +45000000,17,3,81.6,28.8,13,28,without +59850000,8,3,126,17.1,3,29,euro +43000000,10,3,118.6,30.4,9,48,without +53000000,7,3,93.8,20,4,14,euro +55000000,16,3,91,30,13,41,without +64000000,6,3,103,15.2,2,11,euro +124000000,15,3,135,18,2,11,euro +145000000,11,3,95,12,7,14,without +65000000,25,3,75.3,13,17,21,without +23800000,5,3,80,16.7,22,30,euro +36000000,7,3,92.9,20.3,5,17,without +34000000,5,3,72.2,8.4,4,30,cosmetic +35490000,3,3,98.5,19.7,3,4,euro +52000000,10,3,133,17.6,15,52,without +182000000,10,3,184.6,21.9,2,8,without +51000000,18,3,98.2,16.9,18,19,without +11990000,16,3,54,11.1,3,4,without +12900000,13,3,62,10,4,17,without +15500000,8,3,72,12.6,6,8,cosmetic +24800000,4,3,62,6.6,1,6,euro +31800000,22,3,93.5,17.1,11,29,without +74800000,23,3,104,27,14,26,without +44988700,5,3,71.2,6.2,2,14,euro +39500000,17,3,81.25,27.4,6,18,cosmetic +64900000,11,3,90,35.1,7,21,without +12000000,8,3,63,7,1,9,euro +14450000,14,3,60.6,6,2,9,cosmetic +24287000,4,3,84,13.6,11,22,euro +26500000,6,3,80,15,19,24,euro +41990000,5,3,78.2,20,2,7,euro +45000000,11,3,107.3,10,22,29,without +45000000,12,3,85.8,13.7,28,42,without +49990000,12,3,89,14,30,37,without +76000000,5,3,121,15.4,6,12,euro +12800000,5,3,60,8,9,9,euro +24000050,15,3,55,11.2,7,8,euro +30800000,10,3,68.5,12.3,7,52,without +32300000,4,3,85,20,29,29,without +39200000,11,3,117.9,34.6,20,20,without +41000000,1,3,90,20,8,13,euro +46000000,14,3,123.8,34,1,16,euro +58000000,7,3,90,20,14,25,without +67000000,7,3,102.9,15.2,3,9,cosmetic +70000000,3,3,100.9,12.9,16,22,euro +69900000,9,3,102,15.1,7,9,euro +76000000,5,3,120.9,15,6,12,euro +93000000,11,3,141.5,18.3,2,8,without +96000000,11,3,100,14.9,7,18,without +160000000,5,3,114,16.1,2,6,euro +194800000,5,3,160,19.9,47,54,euro +27770000,3,3,67.6,12.2,5,12,without +115000000,3,3,146,18.7,4,9,euro +155000000,10,3,125.3,17,6,14,without +33400000,14,3,85,14.4,8,8,cosmetic +37990000,10,4,94.5,14.5,20,58,without +180000000,10,4,187,22.1,7,11,without +71000000,6,4,135.6,37.4,11,21,without +61988000,14,4,115.6,14.1,3,14,without +43000000,10,4,108.2,15.6,11,58,without +52500000,10,4,127.5,17.2,20,58,without +170000000,15,4,170,20.7,2,5,euro +42000000,6,4,113,16,4,44,without +67000000,4,4,84.8,10,2,8,without +43000000,10,4,108,15.6,11,54,without +155000000,6,4,179.2,21.5,2,6,without +156000000,8,4,140,18.2,5,15,without +180000000,7,4,186.8,22.1,7,11,without +92000000,9,4,169,20.6,6,6,without +155000000,6,4,179.2,21.5,2,12,without +180000000,7,4,187,22.1,7,11,without +40500000,10,4,106.1,15.2,48,58,without +79000000,10,4,145.8,15,36,47,euro +80000000,19,4,186.4,22.1,11,44,without +132000000,5,4,133.5,17.7,10,21,euro +185000000,10,4,171,20.8,4,6,without +61000000,10,4,134.5,17.8,15,52,without +149600000,8,4,174,21,12,13,without +43000000,9,4,118.6,30.4,9,48,without +155000000,10,4,133.1,17.7,6,14,without +180000000,8,4,186.6,27,7,11,without +179000000,9,4,140,18.2,4,8,without +52000000,10,4,107,15.5,14,52,euro +83000000,5,4,147,12,15,24,euro +155000000,6,4,179.2,19.4,2,5,without +180000000,3,4,187,22.1,7,11,without +59900000,12,4,145,20,4,32,without +70000000,25,4,184.5,20,8,8,without +78500000,16,4,132.9,9,7,41,without +81700000,8,4,132,15,2,4,euro +104500000,12,4,118,16.4,3,7,without +163990000,10,4,170,20.7,7,12,without +61488700,5,4,116,15,4,14,without +70000000,5,4,185,21.9,8,8,without +74700000,5,4,165,20.3,8,15,without +70000000,25,4,184.5,21.9,8,8,without +176400000,16,4,195.2,22.8,5,5,euro +43000000,7,4,122,12,15,24,euro +72100000,16,4,105,15.3,27,46,without +80000000,10,4,116,20,5,5,euro +85000000,9,4,160,16,2,6,euro +87000000,5,4,145.2,18.7,7,14,cosmetic +150000000,5,4,155.8,19.5,12,21,euro +60000000,20,4,160,10,31,48,euro +61990000,11,4,92,14.3,9,26,without +95000000,7,4,155,19.5,44,53,without +153000000,3,4,140,8,5,9,without +165000000,8,4,184,15,5,7,without +43400000,3,4,146.5,22,26,28,without +85000000,16,4,186,10,35,42,without +80000000,12,4,144.2,18,8,50,euro +59500000,10,4,108,15.6,21,50,without +32700000,2,4,97,14.7,7,7,euro +51988700,28,4,98,15,5,14,without +52000000,10,4,107.7,15,15,52,without +79900000,16,4,137.6,9.4,8,41,without +98500000,5,4,141.3,20,11,13,without +119000000,8,4,200,23.2,7,8,euro +19800000,9,4,87.1,14.3,6,12,euro +42200000,10,4,102,15.1,7,22,without +48500000,8,4,95.6,9,3,8,cosmetic +48999000,5,4,95.8,13,9,14,without +47000000,2,4,108,12,29,39,without +119000000,2,4,168,15,4,6,euro +18800000,13,4,89.8,8.2,3,14,euro +47900000,6,4,102.4,19,14,20,without +85900000,9,4,131.1,17.5,2,17,euro +120000000,2,4,167.8,10,4,5,euro +130000000,3,4,192,28,7,8,euro +165000000,11,4,182,25,6,10,without +164844000,4,4,137,18,8,9,without +44999000,5,4,101,10,6,27,without +62999000,5,4,143,20,16,27,without +75000000,2,4,145.5,20,2,8,euro +49000000,9,5,122.4,16.8,37,48,without +69500000,9,5,164.4,20.2,37,48,without +60000000,4,5,134.7,11.7,2,4,euro +135000000,19,5,172,20,5,20,without +66900000,6,5,150,19,7,7,without +67000000,10,5,136.8,18,19,52,without +83999000,28,5,135,17.8,7,14,without +55000000,18,5,142,18.4,13,13,without +90000000,12,5,158,19.7,30,37,without +68000000,8,5,136,20,2,6,euro +64999000,11,5,150,15,2,6,euro +61488700,28,5,136,15,2,14,without +100899000,28,5,137,15,8,12,without +120000000,10,5,150,19,20,22,cosmetic +56988700,5,5,134,15,10,25,without +89000000,6,5,189,22,3,5,cosmetic +71999000,12,5,200,21,25,28,euro +48000000,5,5,120,20,2,4,euro +119000000,2,5,170,15,4,6,euro +180000000,8,5,198.8,23.1,4,10,without +54900000,15,5,125,20,3,3,euro +99844800,6,5,190,22.4,13,14,without +179000000,8,5,200,23.2,4,13,without +60000000,9,5,135,17.8,2,7,cosmetic +160000000,8,5,180,35,16,24,without +199000000,6,5,177,18,3,7,euro +53000000,18,5,183.5,30,12,14,euro +61488700,5,5,136,15,2,14,without +110000000,9,5,187.2,22.1,16,17,without +140000000,10,5,181.4,20,4,5,euro +180000000,14,5,178,21.4,6,8,without +199000000,6,5,177,25,3,7,euro +62000000,5,5,136,20,2,14,without +69000000,2,5,198.9,36,10,19,euro +130000000,5,5,172,13,4,5,euro +158000000,8,5,180,35,16,24,without +120000000,6,5,170,20,19,25,without +40900000,3,5,142,14.4,12,22,without +68900000,12,5,160,19.9,21,22,without +148900000,7,6,200,23.2,20,22,without +72157940,11,6,172.9,20,27,27,without +89000000,6,6,189,22,3,5,without +46500000,13,6,187,26.6,9,9,without +88000000,11,6,181.7,13,15,15,without +82000000,8,6,176,21.2,2,6,cosmetic +82000000,13,6,176,21.2,2,6,without +35000000,6,6,88.2,10,12,17,without +38900000,4,6,103.2,14.3,3,6,cosmetic +61999999,15,6,138,18.1,33,46,without +104000000,9,6,161,20,22,28,without +106140000,8,6,183,20,23,24,without +8500000,11,1,33.5,7.6,8,17,without +10350000,8,1,38.3,10,17,22,without +10500000,20,1,34.5,9.5,3,16,without +11700000,27,1,34.4,9,12,16,without +14790000,15,1,32,9.3,3,27,without +19000000,10,1,38,9.8,31,58,without +19400000,11,1,35,6,9,9,euro +21300000,9,1,42.6,10.2,28,58,without +24900000,7,1,41.6,20,2,28,without +33000000,17,1,45.5,6.3,3,40,euro +20000000,9,1,42.4,10.1,18,48,without +22000000,9,1,42.4,10.1,37,48,without +19776000,13,1,41.2,10,13,25,euro +31000000,17,1,40.1,9.1,15,40,euro +31500000,17,1,41.1,4.9,16,40,euro +18800000,6,1,46,10.4,30,41,without +15000000,4,1,33,9,6,28,euro +25000000,8,1,38,15,14,17,euro +16974000,6,1,36.9,9.7,2,8,without +14999000,4,1,43.1,17.1,7,30,without +19044000,6,1,41.4,10.1,2,8,without +16698000,6,1,36.3,9.6,2,8,without +12500000,2,1,37.9,9.6,5,25,cosmetic +22500000,9,1,45.52,10.4,4,22,without +15849000,8,1,38,7,4,6,cosmetic +15000000,6,1,39,10.1,18,33,euro +15600000,5,1,60,9.5,4,9,euro +18500000,1,1,37,15,22,29,cosmetic +20300000,8,1,44,15.6,3,14,without +41670000,6,1,46,10.4,3,5,without +44650000,26,1,47,10,4,5,without +10500000,3,1,39.2,8.6,2,14,cosmetic +16000000,14,1,29,9,7,24,without +16900000,6,1,48.8,16.9,21,30,without +17990000,30,1,40,11,25,48,euro +8500000,4,1,30,8,1,4,euro +8762985,10,1,28.1,4,2,4,euro +9300000,7,1,32,6,5,9,cosmetic +10399000,9,1,36,9.3,12,17,euro +11300000,7,1,37.4,7,2,17,without +11500000,13,1,44.3,9.3,13,17,euro +11600000,10,1,34.2,8.6,7,9,euro +11800000,3,1,31.8,6,4,5,cosmetic +12000000,13,1,35,10,5,25,cosmetic +12500000,15,1,51.4,8,4,5,cosmetic +14000000,21,1,43.5,10,11,24,euro +14000000,6,1,35,9.7,13,14,cosmetic +14000000,9,1,37,16,34,38,without +14500000,6,1,39,8,2,17,euro +16400000,7,1,39,9.9,7,37,without +23000000,6,1,44.1,10.3,5,14,without +24000000,18,1,47,12,18,25,without +25000000,8,1,60.2,17,11,29,euro +25000000,13,1,48,10.6,15,51,without +25300000,8,1,60.2,19,15,29,euro +38400000,10,1,54.1,12.7,13,14,without +8900000,15,1,34.9,9,11,12,without +11900000,4,1,44,11,5,24,euro +13100000,15,1,38.6,5.1,12,24,without +14500000,5,1,39,9,2,17,euro +18882500,6,1,42,10.1,10,14,without +20611500,6,1,45.5,10.4,10,14,without +12500000,18,1,40,9.9,16,24,without +8451880,14,1,39.9,10.5,2,25,without +8597740,14,1,41.9,11.6,9,25,without +8739300,14,1,41.3,10.3,2,26,without +9226240,14,1,41.9,11.6,13,25,without +11350000,7,1,34.7,9.5,2,16,cosmetic +41670000,4,1,46.3,10.5,3,5,without +52000000,5,1,52.1,29,3,3,euro +6650000,15,1,20.6,4,4,22,euro +6650000,17,1,20.6,3,3,22,euro +7762971,9,1,21.4,3,2,4,euro +9500000,11,1,20.3,8.3,2,4,euro +11900000,10,1,32,7,5,8,euro +13290000,6,1,38.1,9.8,8,17,cosmetic +13800000,12,1,45,15,29,31,euro +16400000,5,1,41,10,14,30,euro +22000000,10,1,31.7,5,3,9,euro +36700000,7,1,59,14,3,10,cosmetic +49900000,6,1,60.9,11.7,3,13,without +8500000,25,1,36,10,10,19,cosmetic +11090000,15,1,42.7,18.2,7,16,euro +12700000,11,1,39,9,5,14,euro +13500000,8,1,36,9.6,15,17,euro +10200000,6,1,38.3,9.9,4,18,euro +19750000,8,1,36,16,25,42,without +11500000,6,1,38.9,9,12,12,euro +17500000,11,1,42,10,11,25,euro +17500000,7,1,42,10,11,25,euro +20000000,9,1,42.1,15.4,18,48,without +23000000,12,1,41.6,15.5,3,6,without +4000000,15,1,20,5,2,21,euro +5917500,28,1,22.2,3.3,1,6,without +5970100,28,1,22.7,3,1,6,without +6990000,18,1,25.4,4,1,5,euro +7300000,18,1,29.6,4,1,5,euro +7990000,15,1,24.4,3,1,4,euro +8000000,13,1,33,6,9,9,cosmetic +8950000,11,1,39.2,16.4,4,25,without +9000000,7,1,34,6.6,9,12,cosmetic +9250000,11,1,37.5,7,4,17,euro +9400000,11,1,40,9.5,1,17,euro +9900000,29,1,39.1,13,7,13,without +10500000,4,1,36,9.6,3,10,euro +10700000,7,1,41.8,10.4,19,25,euro +11250000,5,1,40,9.8,1,14,euro +12650000,8,1,35.5,13.8,3,12,euro +13500000,15,1,35.9,9.6,6,33,euro +13938000,7,1,20.2,4,3,9,euro +15050000,7,1,25.3,4,4,9,euro +15000000,28,1,33.7,5.7,6,7,euro +15500000,9,1,30,9.1,7,9,cosmetic +16400000,7,1,49.7,10.8,3,8,cosmetic +19500000,7,1,45.15,10.4,4,21,without +23700000,8,1,41,10,23,24,euro +28000000,12,1,42,10.1,13,42,without +29490000,2,1,45.7,5,6,10,without +19000000,8,1,40,9,10,15,euro +23500000,8,1,49.4,13.7,5,11,without +7542696,7,1,27.8,8.9,2,8,cosmetic +8000000,6,1,32.6,7,2,9,without +9500000,10,1,37.9,8,3,16,without +9500000,13,1,39,13,11,16,cosmetic +13000000,10,1,38.4,9,2,14,euro +28300000,7,2,86.2,13.8,13,14,without +65920000,2,2,82.4,13.5,3,8,cosmetic +62904600,2,2,105.9,15.4,3,8,cosmetic +26000000,11,2,65,15,4,38,cosmetic +20000000,17,2,58.4,10,2,24,without +14400000,8,2,38.6,9.8,4,12,cosmetic +18000000,10,2,68.3,10,8,24,euro +10800000,11,2,42.1,5.3,2,5,cosmetic +11900000,7,2,57.1,10.1,21,22,cosmetic +12600000,10,2,36,6.3,23,33,euro +14000000,22,2,53.5,10,9,12,without +20000000,17,2,58.4,9,2,24,without +28500000,19,2,71.8,13.1,4,24,euro +41000000,17,2,64.5,6.2,11,41,without +47000000,17,2,60,5.5,2,41,without +29000000,7,2,81.1,13.3,11,15,euro +99000000,6,2,90,14.1,5,18,without +30500000,9,2,60.1,11.6,27,58,without +10600000,10,2,45.5,8,10,12,without +29000000,26,2,50,15,12,44,without +41000000,9,2,54,9,8,8,euro +46900000,5,2,98.6,14.8,7,12,euro +49000000,4,2,73,13.1,7,10,euro +29000000,5,2,55.2,11.2,5,8,euro +99000000,6,2,90,14.1,5,18,without +120000000,5,2,113.13,16,7,21,euro +11000000,20,2,44.2,5.5,2,5,cosmetic +34000000,28,2,65.2,13.5,1,14,without +47000000,3,2,73,10,7,10,euro +12250000,19,2,44.7,6,2,9,euro +14150000,7,2,64.2,12,2,14,euro +16950000,9,2,54,9.5,1,17,cosmetic +15900000,3,2,48.4,12,8,12,without +23000000,14,2,50.9,10.9,5,14,without +76960000,4,2,59.2,11.5,6,12,without +83000000,4,2,66,12.1,3,8,without +84900000,8,2,90,14.1,3,9,euro +34500000,21,2,55.5,10.2,38,46,without +25000000,8,2,59.7,18,35,37,without +26000000,17,2,55,10.9,23,27,euro +19250000,10,2,60.8,5.4,1,5,cosmetic +23000000,6,2,45,10,8,14,without +31500000,26,2,70,12.4,22,25,without +40990000,16,2,72,7,6,41,without +11200000,20,2,50,8.3,7,17,cosmetic +11900000,5,2,45,7,3,12,without +16500000,8,2,51,11,14,14,euro +25000000,14,2,71,14,5,16,euro +134400000,3,2,64,11.9,2,5,without +13380000,30,2,44.7,6.7,4,9,euro +22900000,11,2,60,19.6,27,27,euro +35995000,25,2,94.2,14.4,7,8,euro +13700000,21,2,76.2,12,1,3,without +18500000,10,2,44,15,3,5,euro +10500000,14,2,37,9.7,3,9,cosmetic +25900000,3,2,56,11.8,26,27,euro +32000000,6,2,82.9,18.8,4,18,without +40400000,13,2,68,12.3,2,5,without +15500000,5,2,56.1,10.6,1,17,euro +27900000,12,2,55,4,20,43,without +82000000,4,2,65.6,12.1,3,9,without +24500000,18,2,61,11.7,6,13,euro +78000000,4,2,60,11.6,6,12,without +83000000,4,2,66,12.1,3,12,without +99000000,6,2,89.7,14,5,12,without +21900000,7,2,54,12,12,47,without +10941180,14,2,63.8,11.7,4,25,without +12500000,7,2,60.2,11,5,13,cosmetic +12800000,10,2,58.3,7.9,3,3,cosmetic +13000000,5,2,46,6.5,11,12,cosmetic +15800000,8,2,55,9.3,14,17,euro +22000000,8,2,65,13.2,11,20,without +25000000,4,2,71,14,5,16,euro +25499000,11,2,46.2,8,2,8,cosmetic +25800000,1,2,56.7,20,39,58,without +26700000,2,2,57,14,44,59,euro +27199000,5,2,64,10,8,25,without +37500000,7,2,41.7,10.1,3,5,euro +44000000,7,2,44.27,10.3,3,5,without +6950000,10,2,46,8,7,7,without +9550000,7,2,40,10,4,14,cosmetic +10500000,18,2,39,7,7,12,cosmetic +10900000,2,2,39,6,4,9,cosmetic +11000000,5,2,44.9,6,7,14,cosmetic +11760000,10,2,49.7,10.8,11,16,without +12500000,12,2,43.4,5.7,5,5,without +12550000,19,2,52.3,10.2,12,16,cosmetic +12600000,10,2,51.1,10.9,5,9,euro +13500000,13,2,55,8.7,2,24,cosmetic +53000000,9,3,96.9,11.1,2,5,without +36000000,10,3,82.5,27.9,36,53,without +47000000,9,3,103.8,15,6,6,without +170000000,10,3,117,6,3,5,euro +36000000,9,3,83,13.9,36,58,without +37900000,10,3,92.5,16.9,20,53,without +52500000,11,3,92,20,4,21,cosmetic +19990000,7,3,59.2,11.5,6,9,cosmetic +13499000,4,3,66.9,12.2,2,12,cosmetic +13500000,5,3,79.1,8.6,2,14,cosmetic +16900000,25,3,79,11,8,9,cosmetic +18500000,5,3,78,13,4,17,cosmetic +21000000,20,3,82.4,26.2,19,22,euro +45000000,26,3,75.5,12.9,13,26,without +40000000,28,3,69,12.3,7,14,without +46000000,28,3,70,12.4,2,14,without +115732500,6,3,118.7,16.5,3,5,without +44500000,9,3,84,8,2,8,without +45000000,10,3,84.6,10,6,6,euro +33999000,4,3,95.5,14.5,29,29,without +34900000,5,3,61.8,8,3,3,cosmetic +55000000,16,3,91,30,13,41,without +100000000,5,3,90.53,14.1,10,21,euro +24500000,8,3,68.5,9.1,4,6,cosmetic +56650000,17,3,82.2,7.6,37,41,without +97799000,6,3,97.8,14.7,10,10,without +44000000,17,3,87,13.8,15,22,without +170000000,10,3,169,20.6,4,6,without +131000000,5,3,114,16.1,7,21,euro +40500000,10,3,106.1,32.4,48,58,without +43500000,10,3,105.6,15.6,11,53,without +120000000,15,3,100,14.9,6,7,euro +65000000,11,3,90,14.1,7,21,without +182240000,7,3,113.9,17,6,12,without +36500000,25,3,79.5,13.2,4,16,euro +23800000,5,3,80,16.7,22,30,euro +33800000,7,3,90.8,12.6,2,30,cosmetic +45000000,17,3,81.6,28.8,13,28,without +59850000,8,3,126,17.1,3,29,euro +43000000,10,3,118.6,30.4,9,48,without +56000000,16,3,146,18.7,11,31,without +64000000,6,3,103,15.2,2,11,euro +124000000,15,3,135,18,2,11,euro +145000000,11,3,95,12,7,14,without +65000000,25,3,75.3,13,17,21,without +36000000,7,3,92.9,20.3,5,17,without +34000000,5,3,72.2,8.4,4,30,cosmetic +35490000,3,3,98.5,19.7,3,4,euro +89000000,8,3,119.2,16.5,3,11,without +182000000,10,3,184.6,21.9,2,8,without +31800000,22,3,93.5,17.1,11,29,without +51000000,18,3,98.2,16.9,18,19,without +12900000,13,3,62,10,4,17,without +15500000,8,3,72,12.6,6,8,cosmetic +34999999,1,3,91.1,12,6,30,euro +74800000,23,3,104,27,14,26,without +44988700,5,3,71.2,6.2,2,14,euro +144170000,4,3,110.9,15.8,6,12,without +21790000,7,3,65.9,9.7,14,16,euro +25000000,9,3,88.3,9.4,4,5,cosmetic +39500000,17,3,81.25,27.4,6,18,cosmetic +41990000,5,3,78.2,20,2,7,euro +45000000,12,3,85.8,13.7,28,42,without +49990000,12,3,89,14,30,37,without +65000000,8,3,90.1,15,7,21,without +76000000,5,3,121,15.4,6,12,euro +28500000,11,3,70,14,3,20,without +32300000,4,3,85,20,29,29,without +41000000,1,3,90,20,8,13,euro +67000000,7,3,102.9,15.2,3,9,cosmetic +69900000,9,3,102,15.1,7,9,euro +76000000,5,3,120.9,15,6,12,euro +93000000,11,3,141.5,18.3,2,8,without +160000000,5,3,114,16.1,2,6,euro +194800000,5,3,160,19.9,47,54,euro +27770000,3,3,67.6,12.2,5,12,without +115000000,3,3,146,18.7,4,9,euro +37990000,10,4,94.5,14.5,20,58,without +43000000,10,4,108.2,15.6,11,58,without +52500000,10,4,127.5,17.2,20,58,without +43000000,9,4,118.6,30.4,9,48,without +43500000,10,4,105.6,15,11,53,without +24500000,18,4,93,11,1,9,euro +180000000,10,4,187,22.1,7,11,without +19600000,9,4,87.1,14.3,6,12,euro +48500000,8,4,95.6,9,3,8,cosmetic +170000000,15,4,170,20.7,2,5,euro +61988000,14,4,115.6,14.1,3,14,without +42000000,6,4,113,16,4,44,without +155000000,6,4,179.2,21.5,2,12,without +180000000,7,4,187,22.1,7,11,without +45000000,6,4,86.5,8.7,5,9,cosmetic +67000000,4,4,84.8,10,2,8,without +80000000,7,4,193.1,18,2,28,without +85988700,5,4,136.5,13,2,12,without +43000000,10,4,108,15.6,11,54,without +155000000,6,4,179.2,21.5,2,6,without +156000000,8,4,140,18.2,5,15,without +180000000,7,4,186.8,22.1,7,11,without +79000000,10,4,145.8,15,36,47,euro +92000000,9,4,169,20.6,6,6,without +40500000,10,4,106.1,15.2,48,58,without +80000000,19,4,186.4,22.1,11,44,without +132000000,5,4,133.5,17.7,10,21,euro +149600000,8,4,174,21,12,13,without +59970000,10,4,102.5,15.1,3,4,cosmetic +85000000,9,4,160,16,2,6,euro +155000000,10,4,133.1,17.7,6,14,without +179000000,9,4,140,18.2,4,8,without +70000000,25,4,184.5,21.9,8,8,without +83000000,5,4,147,12,15,24,euro +155000000,6,4,179.2,19.4,2,5,without +180000000,3,4,187,22.1,7,11,without +59900000,12,4,145,20,4,32,without +70000000,25,4,184.5,20,8,8,without +78500000,16,4,132.9,9,7,41,without +104500000,12,4,118,16.4,3,7,without +104500000,12,4,118,6.3,3,7,without +163990000,10,4,170,20.7,7,12,without +61488700,5,4,116,15,4,14,without +70000000,5,4,185,21.9,8,8,without +36000000,14,4,105.42,15.4,9,9,cosmetic +164844000,4,4,137,18,8,9,without +176400000,16,4,195.2,22.8,5,5,euro +43000000,7,4,122,12,15,24,euro +72100000,16,4,105,15.3,27,46,without +80000000,10,4,116,20,5,5,euro +85000000,12,4,133,17.6,21,44,without +85900000,9,4,131.1,17.5,2,17,euro +150000000,5,4,155.8,19.5,12,21,euro +185000000,10,4,171,20.8,4,6,without +61990000,11,4,92,14.3,9,26,without +67000000,6,4,115,16.2,9,14,without +85900000,6,4,135,17.8,10,14,without +95000000,7,4,155,19.5,44,53,without +153000000,3,4,140,8,5,9,without +165000000,8,4,184,15,5,7,without +85000000,16,4,186,10,35,42,without +80000000,12,4,144.2,18,8,50,euro +34200000,8,4,114,16.1,14,16,cosmetic +52000000,10,4,107,15.5,14,52,euro +61000000,10,4,134.5,17.8,15,52,without +32700000,2,4,97,14.7,7,7,euro +51988700,28,4,98,15,5,14,without +52000000,10,4,107.7,15,15,52,without +79900000,16,4,137.6,9.4,8,41,without +98500000,5,4,141.3,20,11,13,without +119000000,8,4,200,23.2,7,8,euro +18800000,13,4,89.8,8.2,3,14,euro +48999000,5,4,95.8,13,9,14,without +47000000,2,4,108,12,29,39,without +119000000,2,4,168,15,4,6,euro +49900000,9,5,122.4,16.8,37,48,without +69500000,9,5,164.4,20.2,37,48,without +195699000,6,5,195.7,22.8,10,18,without +60000000,4,5,134.7,11.7,2,4,euro +135000000,19,5,172,20,5,20,without +66900000,6,5,150,19,7,7,without +83999000,28,5,135,17.8,7,14,without +55000000,18,5,142,18.4,13,13,without +90000000,12,5,158,19.7,30,37,without +68000000,8,5,136,20,2,6,euro +67000000,10,5,136.8,18,19,52,without +64999000,11,5,150,15,2,6,euro +100899000,28,5,137,15,8,12,without +61488700,28,5,136,15,2,14,without +115000000,10,5,150,19,20,22,cosmetic +50000000,4,5,84.6,13.6,7,26,without +56988700,5,5,134,15,10,25,without +80000000,18,5,127.3,17.2,7,26,without +89000000,6,5,189,22,3,5,cosmetic +61488700,5,5,136,15,2,14,without +71999000,12,5,200,21,25,28,euro +179000000,8,5,200,23.2,4,13,without +48000000,5,5,120,20,2,4,euro +119000000,2,5,170,15,4,6,euro +180000000,8,5,198.8,23.1,4,10,without +54900000,15,5,125,20,3,3,euro +99844800,6,5,190,22.4,13,14,without +60000000,9,5,135,17.8,2,7,cosmetic +160000000,8,5,180,35,16,24,without +180000000,8,5,198.8,17,4,11,without +199000000,6,5,177,18,3,7,euro +53000000,18,5,183.5,30,12,14,euro +110000000,9,5,187.2,22.1,16,17,without +140000000,10,5,181.4,20,4,5,euro +180000000,14,5,178,21.4,6,8,without +199000000,6,5,177,25,3,7,euro +62000000,5,5,136,20,2,14,without +69000000,2,5,198.9,36,10,19,euro +130000000,5,5,172,13,4,5,euro +46500000,13,6,187,26.6,9,9,without +148900000,7,6,200,23.2,20,22,without +72157940,11,6,172.9,20,27,27,without +89000000,6,6,189,22,3,5,without +35000000,6,6,88.2,10,12,17,without +88000000,11,6,181.7,13,15,15,without +82000000,8,6,176,21.2,2,6,cosmetic +82000000,13,6,176,21.2,2,6,without +38900000,4,6,103.2,14.3,3,6,cosmetic +61999999,15,6,138,18.1,33,46,without +104000000,9,6,161,20,22,28,without +106140000,8,6,183,20,23,24,without +32080036,8,1,47.5,10.6,29,37,euro +24072793,5,1,38.1,14.7,30,41,euro +26890000,1,1,43.5,18.9,11,14,euro +26898520,11,1,45.2,10.4,8,17,euro +39463466,3,1,62.5,11.8,29,41,without +47442265,15,1,51.35,10.9,2,6,without +11000000,15,1,41.5,10.5,9,25,cosmetic +11200000,6,1,36.9,11.7,4,16,cosmetic +18670000,10,1,36.17,9.6,14,28,euro +52452400,16,1,52.4,11,11,18,without +16998097,5,1,40.19,19,3,54,cosmetic +26166400,10,1,48.1,15.5,17,21,euro +21000000,8,1,36.2,4,13,41,euro +23227228,5,1,44.44,10.3,6,57,euro +19166784,6,1,44.8,17,2,25,euro +22726909,8,1,37.7,3.9,7,13,euro +26655920,20,1,40.4,5.9,5,22,euro +20271104,17,1,40.4,11,2,43,euro +29201550,8,1,30.45,9.2,2,14,euro +31727850,8,1,46.9,10.5,13,13,euro +23863000,18,1,49,21.3,4,32,euro +18443384,7,1,41.39,10.1,32,44,euro +35000000,15,1,45,10.4,5,14,euro +17182500,20,1,43.5,11.8,6,28,cosmetic +24851960,16,1,39.1,11.5,2,38,euro +15800000,7,1,33.7,9.4,24,41,cosmetic +19000000,8,1,43.34,5,23,55,euro +25000000,14,1,69.7,15,7,27,euro +31000000,14,1,48.73,22.8,5,8,euro +35980759,14,1,60.05,22.5,5,33,euro +20342989,16,1,45.7,21.1,12,22,euro +15521892,15,1,31.9,10.8,31,48,cosmetic +17602146,15,1,39.7,9.9,14,36,euro +22921100,8,1,48.5,10.7,28,34,euro +31000000,8,1,70.2,12.4,5,19,euro +18677971,4,1,45.31,12.2,4,17,euro +49571000,1,1,52.18,31.2,2,9,without +18695759,9,1,35,10,15,32,euro +18653329,14,1,37.7,15,4,11,euro +18990000,17,1,37.5,14.5,5,43,euro +23175600,13,1,43.4,11.6,3,27,euro +15560203,15,1,29.8,9,5,26,cosmetic +18395650,3,1,37.75,16.7,6,30,euro +23381500,13,1,46.3,10.5,15,25,euro +17130504,19,1,40.1,10,14,24,cosmetic +19150000,14,1,45.1,18.5,21,29,euro +21411928,3,1,41.2,12.4,12,20,euro +8080293,9,1,23.4,8.6,13,14,cosmetic +10832290,6,1,39.4,11.6,10,17,cosmetic +14750000,3,1,37.26,9.7,12,21,cosmetic +16419450,3,1,41.1,10.1,3,21,cosmetic +17880329,2,1,45.6,19.1,2,23,euro +19055000,10,1,41.2,11.9,11,25,euro +20100000,8,1,41.9,18,2,24,euro +20143852,6,1,40.26,10,11,25,euro +21720988,6,1,37.5,10.7,2,16,euro +35591626,11,1,53.3,11,3,41,euro +7038389,15,1,23.8,8.6,2,24,cosmetic +14150136,2,1,34.1,9.5,14,28,cosmetic +15616224,11,1,46.8,10.5,14,21,cosmetic +16150680,20,1,29,9,17,19,cosmetic +17800000,8,1,43.52,17,20,57,euro +26707400,16,1,41,20.4,20,30,euro +12608742,10,1,42.94,10.2,4,16,cosmetic +16500000,12,1,32.5,9.3,8,32,cosmetic +18999980,6,1,44.9,10.4,10,35,euro +24500000,7,1,27.5,4,5,19,euro +7680024,17,1,34.6,12.1,3,16,cosmetic +9708098,13,1,36.9,9.7,9,17,cosmetic +9916679,8,1,35.6,11.3,3,7,cosmetic +17514480,12,1,36.56,16.4,16,21,euro +18489120,6,1,29.63,9.6,31,51,euro +9846549,18,1,33.3,9.8,8,30,cosmetic +10023842,11,1,40.9,10,21,22,cosmetic +18500000,3,1,43,10,6,40,euro +20085912,6,1,47.2,19.9,3,25,euro +20748078,6,1,46.8,18.3,13,25,euro +23375869,3,1,38,9.8,6,41,euro +25436116,3,1,38.8,9.8,2,41,euro +50248000,13,1,55.3,11.2,2,18,without +8500000,10,1,40.4,12,11,15,cosmetic +21562092,13,1,47.2,20.5,8,20,euro +24286694,5,1,37.7,14.6,18,41,euro +31786311,2,1,51.9,7.4,2,26,euro +18286490,13,1,38.77,18.7,4,20,euro +20200660,14,1,38.93,18.8,6,20,euro +26204990,2,1,42.7,6.9,18,43,euro +19045534,18,1,32.3,8.2,19,29,euro +22500000,19,1,48.77,3,13,14,euro +21900000,26,1,39.1,9.9,17,43,euro +22600000,9,1,46,18,6,20,euro +34050000,8,1,81.3,13.4,10,13,euro +19700000,6,1,45,17,25,51,euro +19200000,3,1,43.61,10,17,20,euro +29000000,2,1,40.8,10,15,21,euro +8700000,6,1,33.77,8,4,20,cosmetic +13950000,13,1,36.3,9.6,4,22,cosmetic +17100000,15,1,40.1,17.8,4,16,cosmetic +25000000,7,1,61.3,11.3,6,8,euro +25400000,12,1,41.1,13.8,5,21,euro +28000000,20,1,70.8,15.1,8,33,euro +6324997,10,1,23.93,4.5,6,15,cosmetic +6910078,18,1,32.03,10.9,9,9,cosmetic +7331153,19,1,32.5,14.7,2,17,cosmetic +7489705,8,1,25.4,8.7,2,16,cosmetic +7555400,12,1,32.7,16.3,4,14,cosmetic +7618549,12,1,31.9,10.5,2,17,cosmetic +7809556,12,1,32.3,16.2,16,17,cosmetic +7792964,13,1,36.3,10.5,11,14,cosmetic +7850000,15,1,32.51,14.8,17,17,cosmetic +7906171,18,1,35.9,17.2,10,17,cosmetic +8137000,17,1,41.2,12.9,9,9,cosmetic +8100000,17,1,33,9.4,6,18,cosmetic +8165400,14,1,37.2,13.4,14,14,cosmetic +8200000,12,1,36,18.1,16,17,cosmetic +8209688,11,1,32.28,16.2,17,17,cosmetic +8200000,8,1,23.2,8.6,17,17,cosmetic +8334900,16,1,34.3,8.9,5,12,cosmetic +8534309,17,1,37,11.7,7,15,cosmetic +8651376,17,1,36,11.2,10,15,cosmetic +8878600,17,1,41.2,12.9,6,12,cosmetic +8892400,9,1,37.2,11.5,14,14,cosmetic +8821450,9,1,37.2,11.5,15,15,cosmetic +8843959,31,1,31.8,10.1,17,25,cosmetic +8911193,11,1,35.5,9.6,14,19,cosmetic +8935670,5,1,36.4,14.7,2,16,cosmetic +8904194,13,1,44.4,18.7,9,14,cosmetic +9185700,9,1,40.6,13.6,2,15,cosmetic +9126804,14,1,32.6,10.6,2,17,cosmetic +9271040,18,1,35.6,12.9,2,30,cosmetic +9329878,19,1,46.8,18.4,8,17,cosmetic +9284216,4,1,34,10.5,3,32,cosmetic +9390720,17,1,43.8,11.2,5,9,cosmetic +9366953,31,1,31.7,15.9,14,25,cosmetic +9537011,17,1,33.9,12.3,2,21,cosmetic +9549880,9,1,32.5,10.5,17,17,cosmetic +9501203,14,1,32.6,16.1,20,33,cosmetic +9564970,14,1,43.3,17.2,8,14,cosmetic +9605194,6,1,33,16.1,4,33,cosmetic +9737088,6,1,35.2,10.7,4,13,cosmetic +9705985,13,1,36.1,9.6,12,17,cosmetic +9673541,8,1,36.6,10.8,9,9,cosmetic +9661921,13,1,35.06,11.7,2,15,cosmetic +9660148,9,1,32.5,10.5,11,17,cosmetic +9737184,18,1,34.2,16,13,16,cosmetic +9821420,14,1,43.4,17.2,8,14,cosmetic +9840544,6,1,34.1,10.8,14,15,cosmetic +9795427,13,1,34.97,11.7,12,15,cosmetic +9872741,6,1,34.3,10.9,12,15,cosmetic +9877569,9,1,32.6,16.1,14,17,cosmetic +9990000,8,1,32,9.3,14,17,cosmetic +10036936,5,1,32.44,16.4,4,33,cosmetic +10023882,21,1,33.8,16.8,25,25,cosmetic +10030162,5,1,32.2,14.7,16,19,cosmetic +10226930,18,1,36.1,9.7,15,30,cosmetic +10198573,18,1,36.1,16.6,13,30,cosmetic +10206800,19,1,32.3,16.2,9,33,cosmetic +10179579,4,1,32.3,16.3,15,32,cosmetic +10280447,3,1,34.08,10.6,7,33,cosmetic +10405301,13,1,38.6,9.8,15,17,cosmetic +10490949,19,1,34.4,17,33,33,cosmetic +10467171,3,1,32.4,10.6,33,33,cosmetic +10500000,10,1,45,19,2,21,cosmetic +10500000,6,1,30.2,9.1,21,22,cosmetic +10601441,6,1,34,10.5,16,17,cosmetic +10567656,16,1,32.1,16.1,14,14,cosmetic +10555993,5,1,27.4,3.6,2,22,cosmetic +10599833,5,1,27.4,3.6,5,22,cosmetic +10705030,18,1,38.5,13,12,30,cosmetic +10735695,14,1,28.47,9,5,9,cosmetic +10715248,15,1,33.5,9.4,20,24,cosmetic +10781275,9,1,38,10.9,14,18,cosmetic +10847917,4,1,35.9,16.2,40,53,cosmetic +10840170,8,1,32.1,10.5,17,33,cosmetic +10820930,3,1,36.18,17.3,20,33,cosmetic +10915155,6,1,35,9.7,16,16,cosmetic +10914361,14,1,34.6,12.1,3,15,cosmetic +10924226,11,1,47,10.5,1,19,cosmetic +10986500,11,1,30.1,11.5,10,29,cosmetic +10945180,10,1,33.4,9.4,14,17,cosmetic +10918497,17,1,33.3,9.4,12,16,cosmetic +10935700,3,1,32.3,16,19,33,cosmetic +11031049,11,1,41.7,10.1,15,22,cosmetic +49068328,8,2,74.6,12.8,12,36,without +54720000,12,2,90,14.3,3,12,without +58780000,1,2,74.4,22.7,14,14,without +48858726,7,2,51.2,10.9,28,34,without +40531259,3,2,70.1,12.4,15,41,without +47276605,15,2,55.8,11.3,7,11,without +29210815,16,2,58,22.3,4,27,euro +52100000,13,2,66.7,11.3,4,7,without +23547982,9,2,47.5,10.6,9,22,euro +16850000,7,2,34.3,6.4,3,32,cosmetic +29342500,8,2,50,10.8,2,32,euro +27540000,10,2,59.63,21.5,17,21,euro +31407500,9,2,73.9,19.5,2,21,euro +58893780,11,2,108.6,15.6,9,17,without +17168000,4,2,63.6,19.9,2,16,cosmetic +30664646,5,2,59.57,11.6,3,57,euro +34336326,5,2,70.7,28,28,49,euro +31107719,4,2,71.5,12.6,5,20,euro +37350677,8,2,65.7,5.4,6,13,euro +32456220,13,2,66.7,13.3,8,13,euro +21687330,18,2,42.7,10.2,7,42,euro +36453150,8,2,41.19,10,2,14,euro +22512000,20,2,56,14.9,18,28,euro +22813650,8,2,65.5,12.1,18,34,euro +25814040,14,2,66.36,12.1,8,55,euro +26281000,18,2,64.1,16.1,32,32,euro +27000000,7,2,78,18.4,15,52,euro +32354969,18,2,69.1,16.5,9,29,euro +24000000,15,2,56.1,12,8,27,euro +24000000,15,2,56.1,12,8,27,euro +24000000,15,2,56.1,12,8,27,euro +31000000,14,2,68.7,4.4,8,13,euro +39856000,18,2,47,3.7,5,8,without +59183800,7,2,114.92,15,4,19,without +31616040,16,2,48.7,10.3,6,38,euro +50983800,13,2,85.4,22.1,7,17,without +27060879,16,2,61.7,15.8,6,25,euro +52168824,14,2,74.6,12.8,23,26,without +12004469,5,2,46,23.6,6,25,cosmetic +31283538,10,2,74.1,12.8,35,43,euro +22395960,8,2,46.6,10.5,30,34,euro +28905922,4,2,71.31,37.3,4,17,euro +36000000,5,2,94.2,14.4,7,8,euro +35964753,14,2,109.7,15.7,37,58,euro +26814806,13,2,55.9,10.9,23,27,euro +54000000,6,2,54,11.1,4,21,without +62000000,6,2,58.54,11.5,3,15,without +28782941,9,2,65.3,14,31,33,euro +22588696,14,2,54,11.1,3,21,euro +26605000,13,2,62.6,11.8,8,25,euro +28407622,3,2,70.5,23.7,8,30,euro +18902704,19,2,44.1,10.3,12,23,euro +26329387,1,2,45.6,20.4,7,19,euro +26900000,21,2,62.6,24.9,16,42,euro +27493393,3,2,67.6,20.3,3,24,euro +14117848,6,2,37.9,17.5,14,15,cosmetic +15930000,12,2,53.1,7.2,3,21,cosmetic +19385450,4,2,57.1,12.3,2,27,euro +21212323,4,2,57.91,11.4,2,28,euro +28840000,14,2,76.9,23.7,24,29,euro +42715100,8,2,76.41,13,2,11,without +62000000,5,2,58.54,22.5,3,15,without +24466924,2,2,54.7,13.9,2,23,euro +65008835,11,2,81.7,13.4,2,40,without +69900000,9,2,68,12.3,14,21,without +15250624,5,2,49.2,10.1,13,16,cosmetic +23000000,10,2,57.1,14,24,29,euro +26000299,6,2,54.2,25.3,3,16,euro +18654300,10,2,44.1,5.9,2,15,euro +21761000,11,2,92.6,10.1,3,21,euro +26829348,14,2,62.2,11.8,15,24,euro +30171525,2,2,68.3,15.4,23,47,euro +69293014,5,2,65.43,12.1,2,13,without +12772900,17,2,51.5,10.7,6,12,cosmetic +13466437,9,2,47,3.7,4,10,cosmetic +14788454,9,2,40.4,19.1,17,47,cosmetic +20500000,7,2,60.9,22.1,9,33,euro +38326230,16,2,72.3,29.4,14,30,euro +39600000,5,2,76.73,8.5,23,41,without +59900000,5,2,55,11.2,10,21,without +62000000,6,2,58.18,11.5,3,21,without +8734352,17,2,40.5,14,7,15,cosmetic +11070148,5,2,49.9,10.2,11,17,cosmetic +13187768,13,2,54.7,11.2,9,9,cosmetic +14824568,10,2,44.84,10.3,6,21,cosmetic +24872380,6,2,45.89,16,28,51,euro +25125120,13,2,56.29,18.9,16,21,euro +25779772,6,2,64.4,12,5,35,euro +13156022,11,2,63.8,11.9,15,19,cosmetic +13345709,18,2,55.8,13.2,21,30,cosmetic +25800000,5,2,49,10,30,41,euro +54826000,2,2,56.58,11.3,3,21,without +62046000,1,2,76.6,14,4,20,without +28800000,1,2,53.3,7,10,19,euro +28800000,5,2,56.6,11.3,28,41,euro +12703654,14,2,53.6,12.4,12,18,cosmetic +23400000,7,2,59.9,11.2,4,32,euro +18500000,7,2,54.2,10,6,52,euro +22900000,6,2,42.12,20,6,53,euro +23900000,29,2,48.7,10.7,26,40,euro +21499000,13,2,60.8,12,7,46,euro +27000000,13,2,71,20,16,20,euro +40500000,5,2,76,6.8,9,41,without +68000000,10,2,49,10.7,3,6,without +25499000,17,2,64.3,12.6,5,46,euro +50040000,7,2,55.6,11.2,12,46,without +65569500,10,2,48.57,10.7,5,6,without +12450000,9,2,53.7,11.8,9,15,cosmetic +24500000,21,2,64.2,21.3,7,17,euro +32000000,14,2,50,13,4,8,euro +38500000,5,2,77.7,13.1,9,24,euro +25780000,8,2,46,15,11,41,euro +12200000,7,2,49.6,10.2,7,16,cosmetic +20863376,13,2,42.44,22.3,3,20,euro +21497000,6,2,54.3,6,30,52,euro +26548746,18,2,49.7,8.3,10,29,euro +29670742,16,2,58.3,22.6,9,27,euro +31000000,14,2,48.73,10.7,5,8,euro +32074680,13,2,66.52,24,16,20,euro +33711793,3,2,60.3,11.6,6,41,euro +36021487,3,2,63.3,11.9,6,41,euro +36530631,5,2,57.6,19.9,24,41,euro +37628117,13,2,73.42,12.7,7,20,euro +38593280,18,2,70.4,16.9,22,29,without +39618984,3,2,69.5,12.4,8,41,without +40279000,18,2,47,3.7,8,8,without +41400000,5,2,78,8.6,32,41,without +44000000,13,2,59,25,12,12,without +50488727,7,2,49.3,10.7,20,34,without +50979269,7,2,49.3,10.7,21,34,without +52100100,18,2,65.7,16.5,5,8,without +51705900,18,2,65.7,16.5,3,8,without +25554869,6,2,63.1,17.1,16,25,euro +28849099,4,2,66.2,20.3,9,20,euro +34500000,2,2,50.3,10,9,21,euro +47984900,11,2,71.3,12.5,15,17,without +25000000,6,2,65,12,6,43,euro +37000000,2,2,61,20,17,21,euro +16558000,8,2,42.5,10.2,25,34,cosmetic +16723080,8,2,45.1,10.4,15,28,cosmetic +30587700,4,2,68.2,25,11,20,euro +33930000,2,2,37.7,17.8,20,21,euro +50500000,7,2,73.88,25.8,10,19,without +54000000,25,2,54,11.1,4,21,without +25500000,6,2,55,12,9,24,euro +26780000,8,2,46,15,17,41,euro +27500000,25,2,85,27.8,10,28,euro +28000000,29,2,69,11.9,3,15,euro +28200000,3,2,60,12,5,48,euro +30825000,8,2,41.1,12,3,13,euro +38500000,6,2,70,24,13,36,euro +30000000,17,2,71,17,12,41,euro +61552050,11,2,108.5,15.6,11,17,without +35250000,5,2,47,10.5,12,21,euro +64408973,5,3,88.2,13.9,33,36,without +51840000,12,3,81.2,21.9,7,12,without +40000000,6,3,95.8,20.4,9,17,without +58960000,12,3,95.4,28.2,8,14,without +60654000,3,3,91.9,14.2,35,41,without +59155379,15,3,85.3,13.7,5,11,without +38972930,16,3,79.4,25.2,9,27,without +68390000,11,3,98.9,9.5,2,21,without +37005026,9,3,80.8,13.3,18,22,euro +65325902,10,3,74.2,12.8,18,32,without +61135350,11,3,108.3,15.6,12,17,without +34380000,10,3,79.64,13.2,2,21,euro +37636906,5,3,93.85,34,32,54,euro +40450100,10,3,98.9,21.7,3,21,without +24500000,19,3,75.7,24.7,6,30,euro +51058942,5,3,102.15,15.1,44,57,without +30399454,6,3,75.7,17.7,13,25,euro +57463509,8,3,99,29.1,7,13,without +66540960,13,3,131.4,11.1,7,9,without +31605210,19,3,66.3,12.1,11,42,euro +61427720,8,3,79.88,13.2,2,14,without +31381922,17,3,63.5,6.8,9,43,euro +33919110,7,3,85.98,13.8,29,44,euro +39625600,18,3,85.4,15.4,32,32,without +20534220,8,3,60.2,11.6,3,22,euro +39395600,16,3,74.5,15,17,38,without +46500000,18,3,95.78,10.1,20,20,without +51758000,7,3,73.94,5,3,19,without +41946660,6,3,97.2,17.3,8,41,without +38657606,16,3,86.4,23.4,10,25,without +37941057,14,3,113.63,27.8,2,8,euro +52646486,14,3,93.16,25.2,6,33,without +28919022,10,3,71.7,13.6,20,30,euro +21870697,5,3,79.6,35.3,11,25,euro +36905610,10,3,86,13.8,43,43,euro +44513860,6,3,89.8,14.1,31,34,without +28646229,13,3,65.5,9,2,27,euro +45170359,9,3,97,37.7,10,32,without +29070417,14,3,74.4,25.4,7,11,euro +41111970,3,3,89.52,22.8,27,30,without +28377549,15,3,72,24,17,26,euro +35351280,11,3,88.8,14,24,25,euro +39580686,1,3,80,26.1,2,19,without +51729509,3,3,104.8,31.4,4,9,without +21828500,12,3,74.5,7.8,2,21,euro +29078280,19,3,78.1,13.1,19,24,euro +38880000,14,3,111.4,32.5,14,29,without +15507197,6,3,60.8,18.7,12,12,cosmetic +22500000,19,3,58,12,5,22,euro +29492791,4,3,82.79,13.5,3,28,euro +46579795,13,3,66.9,12.2,10,41,without +26757800,4,3,79.4,12.5,12,27,euro +37500000,17,3,83.7,13.6,19,43,euro +10800000,6,3,54.5,11.1,6,24,cosmetic +28254831,6,3,64.6,19.6,2,16,euro +30329603,5,3,73.6,13.4,14,26,euro +16648258,8,3,74.8,12.3,2,15,cosmetic +24052400,10,3,62.8,3.7,6,19,euro +32892135,2,3,88.1,13.9,39,47,euro +13046913,14,3,62.8,19.5,2,10,cosmetic +26610400,11,3,107.3,11.7,3,21,euro +32203990,14,3,82.3,13.4,5,24,euro +39219732,10,3,84.3,23.1,19,32,without +12322500,17,3,75,14.5,2,16,cosmetic +12834125,5,3,62.1,18.7,11,16,cosmetic +16752815,13,3,76.5,13,12,17,cosmetic +17924134,3,3,85.7,22.6,2,16,euro +19553240,10,3,66.59,12.1,6,21,euro +23937900,18,3,65.92,12.1,3,21,euro +34531280,5,3,88.09,16,11,41,euro +51652146,14,3,102.5,15.1,17,35,without +16836145,18,3,72.3,16.9,17,30,cosmetic +17368548,11,3,87.8,13.9,18,22,cosmetic +43900000,14,3,75,5.7,10,14,without +38000000,27,3,77.5,13.1,29,41,euro +41450000,14,3,68.4,5.5,3,14,without +31000000,21,3,58.5,9.4,14,42,euro +30900000,6,3,91.7,35.5,18,54,euro +26700000,12,3,79,21.2,13,47,euro +27000000,6,3,74.1,10.2,4,20,euro +44950000,17,3,92.1,33.3,38,46,without +41760000,16,3,80.3,8,12,32,without +42150000,16,3,84.3,5,9,32,without +48000000,7,3,67,5,6,8,without +30499000,4,3,68.7,22.1,8,13,euro +39900000,6,3,85.5,13.5,19,29,without +28800000,14,3,88.5,14,35,47,euro +31900000,10,3,61.2,14.7,4,25,euro +44900000,4,3,71,13,22,46,without +36500000,8,3,90.58,18.6,10,21,euro +28900000,12,3,64.1,11.9,2,33,euro +52227450,2,3,99,6.3,16,43,without +52804444,14,3,95.07,24.6,3,33,without +67138235,14,3,104.74,24.7,10,33,without +70938390,17,3,86.6,11.8,38,43,without +32054847,6,3,76.7,18.2,21,25,euro +32500000,18,3,66.3,24,19,46,euro +41027287,13,3,88.8,26.4,2,20,without +31101659,14,3,68.8,12.3,13,20,euro +36645828,3,3,83,24.7,20,30,euro +65043300,7,3,92.26,10,14,19,without +65469600,11,3,108,15.6,15,17,without +26500000,12,3,58.3,11.5,15,33,euro +28226582,3,3,64.6,5.2,18,30,euro +32524747,6,3,79.2,18.5,18,25,euro +56500000,5,3,99,7.6,23,41,without +31000000,17,3,72.6,20.9,21,41,euro +35987000,30,3,79.6,13.2,8,25,euro +39526452,13,3,89.51,14,7,20,without +40000000,6,3,76,15,9,41,without +51900000,3,3,136,17.9,25,32,without +62000000,6,3,98,25,7,41,without +67680000,17,3,84.6,13.6,17,32,without +28900000,24,3,82.2,15.3,9,28,euro +43999387,16,3,79.5,25.2,23,27,without +24490000,7,3,59,15.4,2,27,euro +28900000,7,3,64.1,11.9,2,33,euro +30500000,3,3,60,20,31,40,euro +31000000,6,3,65.82,5.3,8,55,euro +37000000,8,3,73.6,18,2,19,euro +39500000,9,3,107.8,7,31,34,without +68810269,8,4,108.1,15.6,9,37,without +52877241,16,4,104.2,15.3,23,27,without +42374125,9,4,93.2,14.4,3,22,without +70417039,8,4,121,16.7,14,32,without +37860840,18,4,83.8,13.6,30,42,euro +43521501,6,4,107,30.3,14,25,without +58103220,16,4,110.7,13,18,23,without +71574300,18,4,162.3,36.3,4,32,without +33851500,20,4,85.7,15.3,6,28,euro +48057113,16,4,110.5,23.7,19,22,without +71603400,16,4,104.5,12.4,30,48,without +25593861,18,4,114.9,26,19,24,euro +37156241,13,4,86,23.8,33,45,euro +41941550,8,4,93.1,14.3,30,34,without +37466440,3,4,93.8,8.1,17,30,euro +41364600,13,4,106.5,15.5,25,25,without +43289259,9,4,82.9,28.3,16,33,without +44371808,14,4,96.4,33.6,14,17,without +70000000,5,4,184.5,21.9,8,8,without +14819084,15,4,64.6,18,3,16,cosmetic +36983095,6,4,87.1,25.3,2,16,euro +20736849,5,4,85,26.3,2,16,euro +31330000,4,4,96.4,12.3,24,27,euro +70835210,13,4,108.4,15.6,18,41,without +35370450,10,4,94.7,7.2,9,25,euro +36763452,3,4,135.2,33.8,16,16,euro +46443600,2,4,116.4,15.2,43,47,without +33641055,11,4,110.1,15.7,2,21,euro +45846892,18,4,105.4,11.8,4,19,without +53417232,11,4,129.6,20,32,49,without +13806217,17,4,77.9,20.3,12,15,cosmetic +41592684,10,4,103.6,15.2,14,35,without +17614465,5,4,104.5,26.9,2,19,euro +25572839,13,4,109.6,15.7,13,17,euro +47933200,8,4,100,16,30,51,without +21706077,11,4,104.1,15.3,20,22,euro +17346768,18,4,80.8,17.7,15,30,cosmetic +27900000,10,4,82,13.4,6,43,euro +36000000,18,4,129,15.2,13,30,euro +43957847,6,4,107,30.3,16,25,without +45250876,6,4,106.9,15.5,22,25,without +45025638,6,4,106.9,15.5,21,25,without +54557930,16,4,104.3,15.3,25,27,without +60045382,17,4,99.6,27.5,25,28,without +67405020,16,4,110.9,13,21,23,without +49900000,23,4,95.7,29.4,5,23,without +33800000,6,4,105,15,13,43,euro +53000000,17,4,124.6,19,4,21,without +11932985,17,4,64.8,18.1,2,16,cosmetic +13119242,14,4,65,17.3,2,14,cosmetic +13221767,5,4,64.7,18.6,6,16,cosmetic +13240292,15,4,65.5,16.6,4,24,cosmetic +13796265,17,4,65.5,16.4,9,15,cosmetic +13806878,15,4,62.1,16.4,8,15,cosmetic +14111608,17,4,78.6,18.5,11,16,cosmetic +14500000,15,4,80.4,7.1,8,13,cosmetic +14769112,9,4,66.7,16.1,3,18,cosmetic +15692106,13,4,69.6,17.2,9,18,cosmetic +15912890,8,4,79,20.6,7,21,cosmetic +16254820,17,4,86.6,22.5,9,9,cosmetic +16897782,8,4,79,20.7,7,21,cosmetic +16936231,10,4,72.9,24.6,2,16,cosmetic +16989468,16,4,85.7,13.7,11,14,cosmetic +17058350,16,4,109.7,15.7,5,14,cosmetic +17212014,18,4,80.8,17.7,6,30,cosmetic +17337320,17,4,86.6,22.5,6,9,cosmetic +17545160,17,4,86.6,22.5,7,9,euro +17597242,11,4,61.9,16.4,11,16,euro +18091303,18,4,83.7,21.4,14,30,euro +17958539,18,4,83.6,21.9,14,30,euro +18540787,6,4,101.3,14.4,14,15,euro +19034498,9,4,87.8,22.1,9,18,euro +19039582,7,4,66.7,3,4,9,euro +19173330,17,4,93,28.5,7,21,euro +20099898,13,4,64.7,4.7,5,14,euro +21535947,12,4,73.6,18.3,12,18,euro +21500316,11,4,104.6,15.3,21,22,euro +21476447,5,4,71.3,3,8,16,euro +21761420,11,4,104.1,15.3,22,22,euro +22146076,7,4,104,15.2,18,20,euro +24356065,13,4,107.2,15.5,15,17,euro +24229338,9,4,82.4,31,32,47,euro +24810532,13,4,107.3,15.5,17,17,euro +25089760,8,4,82.1,13.4,2,28,euro +25869284,18,4,108.5,23.8,18,24,euro +25962400,14,4,78.2,4.8,24,26,euro +26143344,10,4,84.7,31.3,21,30,euro +26240761,7,4,73.72,7.3,24,34,euro +26246250,13,4,109.6,15.7,16,17,euro +26407999,14,4,80.58,13.3,4,13,euro +26258664,10,4,84.6,31.3,30,30,euro +26509241,10,4,84.7,31.3,25,30,euro +26917086,14,4,77.74,13.1,11,25,euro +26823067,14,4,80.58,13.3,9,13,euro +27178614,18,4,115,7.5,21,24,euro +27447095,7,4,73.75,6.9,24,34,euro +27607735,7,4,73.72,7.3,20,34,euro +28650000,14,4,95.5,5.2,3,12,euro +28712320,8,4,95.2,14.5,2,22,euro +29473982,11,4,101.4,15,2,29,euro +29396950,8,4,110,15.2,4,30,euro +29778030,8,4,87.3,13.9,13,34,euro +29915835,11,4,86.7,7.5,26,35,euro +29915127,20,4,108.6,15.6,4,20,euro +30273039,11,4,86.7,7.5,29,35,euro +30719235,11,4,88.5,8.3,25,35,euro +31089000,4,4,96.4,12.3,20,27,euro +31573676,11,4,86.6,5,2,32,euro +31597181,13,4,80.62,6,3,21,euro +31584900,8,4,82.9,13.5,22,34,euro +31622298,11,4,102.6,15.1,16,29,euro +31962635,13,4,79.26,6,15,21,euro +31931846,11,4,102.6,15.1,22,29,euro +32270000,4,4,100,14.8,17,25,euro +32018337,11,4,87.1,5,4,32,euro +33000000,14,4,101.4,17.9,2,28,euro +33541452,4,4,82.8,13.5,3,19,euro +33389132,6,4,108.52,15.6,17,21,euro +33630300,10,4,94.6,7.2,6,25,euro +33743270,17,4,86.9,24.2,2,27,euro +34500000,8,4,89,14,44,55,euro +34892872,13,4,80.62,6,5,21,euro +34842500,8,4,110,15.2,7,30,euro +35319989,8,4,100.89,15,29,49,euro +35424600,8,4,90.6,14.1,5,26,euro +35968200,8,4,90.6,14.1,8,26,euro +35722500,8,4,110,15.2,15,30,euro +36350270,17,4,86.9,24.2,9,27,euro +36333000,11,4,110.1,21.8,22,29,euro +36196644,6,4,88.6,10.1,3,29,euro +36663300,11,4,110.1,21.8,24,29,euro +36794055,13,4,108.2,21.8,31,32,euro +36482000,8,4,92.5,14.3,28,34,euro +36696529,3,4,93.8,8.1,13,30,euro +36846700,10,4,94.6,7.2,18,25,euro +36899520,6,4,95.2,14.5,17,34,euro +37513430,13,4,86.5,5.6,35,45,euro +41457240,8,5,108.3,15.6,33,34,without +18565735,17,5,94.7,23.3,5,14,euro +18476668,17,5,94.7,23.3,4,14,euro +18934626,17,5,94.7,23.3,6,14,euro +26398530,10,5,123.3,26.4,2,17,euro +28694400,10,5,122,26,15,17,euro +29578472,6,5,128.7,30.7,2,15,euro +31947461,6,5,136.7,34,2,15,euro +32034090,6,5,138,33.8,2,12,euro +34468676,16,5,187.8,18,2,23,euro +39356300,8,5,103,15.2,27,28,without +41834240,8,5,112.7,16,21,22,without +41698020,8,5,109.1,15.7,27,28,without +46213200,8,5,118.8,16.5,29,34,without +47373170,12,5,118.7,16.5,26,27,without +48591218,10,5,122.9,16.8,5,35,without +48742897,10,5,123.3,16.8,5,35,without +49756302,10,5,119.3,30.5,24,30,without +49797539,10,5,116.1,30,20,30,without +50621197,14,5,112.1,15.9,10,13,without +50352862,10,5,119.7,30.5,27,30,without +50914920,12,5,123.7,15.7,15,27,without +51632454,14,5,107.6,25,20,21,without +51392502,14,5,114.4,23.1,7,9,without +52156500,19,5,109,15.7,37,42,without +53006100,17,5,117.4,27.3,15,33,without +52806520,17,5,117.4,27.3,13,33,without +53675280,17,5,117.4,27.3,23,33,without +55464261,15,5,119.5,4.4,26,26,without +57084300,6,5,143.5,18.5,34,34,without +60406892,18,5,126.2,11.8,14,19,without +60636576,18,5,126.2,11.8,15,19,without +60062366,18,5,126.2,11.8,13,19,without +68758710,19,5,147.9,18.9,41,42,without +68385177,11,5,153.1,20,26,49,without +70000000,12,5,143,18.4,38,47,without +18648091,17,5,94.7,23.3,6,14,euro +18753818,17,5,94.7,23.3,4,14,euro +22077873,7,5,114.76,10.1,1,17,euro +27183000,10,5,123,26.3,3,17,euro +27515520,10,5,122.4,26,6,17,euro +27368640,10,5,122.4,26,5,17,euro +27639800,10,5,122.3,26,7,17,euro +28021950,10,5,122.1,26,10,17,euro +27908860,10,5,122.3,26,9,17,euro +28156260,10,5,122.1,26,11,17,euro +28560200,10,5,122,26,14,17,euro +28793520,10,5,121.8,26,17,17,euro +28793520,10,5,121.8,26,16,17,euro +28829158,8,5,125.7,16.5,4,16,euro +28682874,8,5,125.8,16.5,3,16,euro +29800563,8,5,125.4,16.5,11,16,euro +31452763,11,5,137.6,18,2,13,euro +33543358,5,5,136.9,34,2,15,euro +33842369,11,5,164.2,37,2,16,euro +34067199,11,5,151.8,33.7,2,16,euro +34217493,11,5,152.1,34,2,13,euro +34632798,11,5,165,30.8,2,16,euro +34857754,6,5,141.6,9.9,2,15,euro +36264749,11,5,149.3,34.7,2,16,euro +42760640,8,5,108.2,15.6,34,34,without +43418560,8,5,112.6,16,28,28,without +43763400,8,5,118.6,16.5,33,34,without +51108272,10,5,120.1,30.9,23,30,without +51708001,10,5,120.5,30.9,26,30,without +52136909,19,5,139.02,6.2,30,30,without +53009297,14,5,155.7,27.3,1,17,without +57044660,17,5,117.4,27.3,26,33,without +57115100,17,5,117.4,27.3,27,33,without +57736800,17,5,131.22,26.1,28,31,without +59920000,6,5,149.8,19,11,11,without +60060058,6,5,132.6,17.1,2,28,without +64445440,17,5,131.2,32.9,26,27,without +64889951,7,5,104.4,15.3,9,18,without +64518105,17,5,159.7,19.9,6,27,without +66242889,11,5,180.9,7.5,41,42,without +66767341,6,5,132.4,17.1,5,28,without +67058413,17,5,157.4,19.7,8,27,without +67238814,6,5,132.4,17.1,8,28,without +68429112,3,5,152.63,18,2,19,without +68853400,10,5,147.28,18.8,24,24,without +68546354,9,5,128.9,18.2,11,28,without +69206400,17,5,144,18.6,26,27,without +70269186,17,5,157.4,19.7,12,27,without +70629469,6,5,126.5,21.5,13,28,without +70821893,6,5,134.1,22.4,7,28,without +71301613,11,5,143.9,6.1,41,42,without +23666056,5,6,33.5,9.4,19,36,euro +17290000,1,6,27.1,7.7,10,14,cosmetic +28913370,9,6,116.9,5.4,10,10,euro +21768120,3,6,33,9.4,2,41,euro +15484667,17,6,23.8,5,2,28,cosmetic +12640000,10,6,23.8,8.6,7,26,cosmetic +24183900,11,6,39,9.9,9,17,euro +14833876,8,6,26.21,3.6,2,36,cosmetic +17966961,8,6,33.8,21.9,11,13,euro +6500000,10,6,21.73,3.5,3,21,cosmetic +7479000,14,6,25.1,5.8,41,45,cosmetic +19125000,16,6,36,9.6,10,23,euro +23052000,18,6,45.2,9,2,32,euro +14555343,16,6,25.4,4.8,18,22,cosmetic +28324800,7,6,33.72,3,6,19,euro +8225616,5,6,23.8,8.6,5,25,cosmetic +10548686,4,6,30.34,9.1,3,39,cosmetic +15400000,15,6,33,6.5,27,43,cosmetic +28245000,1,6,26.9,5,2,9,euro +13198144,14,6,22.6,5,4,11,cosmetic +12441000,12,6,37.7,9.8,3,21,cosmetic +12600000,13,6,51.6,10.9,4,17,cosmetic +13336534,15,6,26,5.3,22,26,cosmetic +13786589,3,6,27.9,5.2,2,30,cosmetic +9905100,3,6,24.1,5.3,27,27,cosmetic +10982050,10,6,23.9,3.8,8,25,cosmetic +15779006,6,6,24.5,5.6,3,16,cosmetic +7241920,10,6,22.4,5.4,2,13,cosmetic +6900728,14,6,21.3,5.3,2,14,cosmetic +9319050,9,6,35.4,3.1,2,10,cosmetic +6326160,17,6,24,3.6,5,16,cosmetic +7287232,8,6,26.3,3.1,9,9,cosmetic +8103679,11,6,22.35,8.5,4,21,cosmetic +17098860,16,6,31.8,3.6,25,26,cosmetic +6056623,11,6,21.1,8.4,12,19,cosmetic +7284959,18,6,22.4,5,3,30,cosmetic +12248520,13,6,28.12,9,7,21,cosmetic +16026504,7,6,26.4,4.2,23,43,cosmetic +16000000,7,6,28.9,10,34,41,cosmetic +14054360,13,6,26.57,5.2,5,20,cosmetic +14367059,13,6,24.93,4.6,12,20,cosmetic +15548571,13,6,27.73,5.5,13,20,cosmetic +15433723,13,6,26.56,5.1,7,20,cosmetic +9370000,10,6,25.4,8.7,13,27,cosmetic +14500000,17,6,30,10,2,41,cosmetic +21900000,26,6,43.2,10.2,23,43,euro +30900000,7,6,40,4,14,19,euro +5745000,13,6,28.9,5.3,7,9,cosmetic +14500000,13,6,29.31,10,15,35,cosmetic +22999999,5,6,63,11.9,4,19,euro +4812800,20,6,20.48,8.3,3,21,cosmetic +5500000,20,6,21.89,8.5,4,21,cosmetic +5640000,20,6,21.67,8.4,4,21,cosmetic +5678248,13,6,22,5.6,3,14,cosmetic +5665000,20,6,21.88,8.4,3,21,cosmetic +5653084,12,6,20.1,8.3,2,14,cosmetic +5707716,17,6,21,5.1,4,16,cosmetic +5974995,13,6,23.8,6,6,14,cosmetic +5940699,13,6,21.34,8.4,2,15,cosmetic +5987508,12,6,20.1,8.3,11,14,cosmetic +6133884,9,6,20,5.1,10,17,cosmetic +6218170,14,6,21.1,3.5,9,14,cosmetic +6236933,17,6,20.4,5.1,13,15,cosmetic +6310899,11,6,23.6,8.6,1,19,cosmetic +6357768,17,6,24,3.6,6,16,cosmetic +6497400,17,6,29.4,5.8,14,14,cosmetic +6490618,13,6,23.8,5.9,17,17,cosmetic +6536260,5,6,20.9,5.6,6,9,cosmetic +6642980,13,6,20.5,8.3,1,9,cosmetic +6627660,13,6,24.38,8.7,3,15,cosmetic +6696984,4,6,20,5.1,5,19,cosmetic +6757475,11,6,22.8,8.5,19,19,cosmetic +6792978,10,6,20.1,8.3,7,21,cosmetic +6838770,14,6,25.7,4.2,4,14,cosmetic +6812374,6,6,20.3,5.1,9,33,cosmetic +6939900,10,6,22.6,5.5,2,16,cosmetic +6929940,5,6,20,5.2,3,19,cosmetic +6933992,4,6,20,5.2,14,19,cosmetic +6935109,14,6,21.3,5.3,2,14,cosmetic +6900383,11,6,22,8.5,1,19,cosmetic +6923590,16,6,27.8,8.9,2,14,cosmetic +6977550,14,6,25.7,4.2,9,14,cosmetic +6977550,14,6,25.7,4.2,11,14,cosmetic +6952223,6,6,20,5.1,16,16,cosmetic +6981936,14,6,20.1,5.1,27,33,cosmetic +6974903,9,6,20,5.1,9,9,cosmetic +6968814,5,6,24.2,5.6,11,16,cosmetic +6997897,6,6,21.2,4.5,7,13,cosmetic +7064820,17,6,29.4,5.8,13,14,cosmetic +7050120,17,6,29.4,5.8,8,9,cosmetic +7017780,17,6,29.4,5.8,6,14,cosmetic +7051207,4,6,21.9,5.7,5,19,cosmetic +7115581,14,6,20.1,5.1,7,17,cosmetic +7160325,18,6,22.2,4.6,13,30,cosmetic +7201389,5,6,20.4,6.9,16,17,cosmetic +7200000,10,6,24.5,5,6,23,cosmetic +7251565,5,6,20.1,5.1,2,33,cosmetic +7267587,11,6,27.1,8.9,18,19,cosmetic +7322291,9,6,21.9,5.5,11,17,cosmetic +7333853,3,6,20.02,5.1,18,33,cosmetic +7317695,19,6,20.1,5,23,33,cosmetic +7420587,5,6,24,5.7,14,19,cosmetic +7469211,18,6,22.4,5,8,30,cosmetic +7553416,9,6,23.6,5.9,7,17,cosmetic +7550000,7,6,27.4,8.9,17,25,cosmetic +7680709,18,6,23.1,5,10,30,cosmetic +7746900,6,6,25.5,6.4,3,33,cosmetic +7728366,5,6,23.74,6,2,33,cosmetic +7724430,7,6,20.1,5.1,18,33,cosmetic +7702320,7,6,20.1,5.1,28,33,cosmetic +7789596,15,6,21.9,5,17,30,cosmetic +7769425,15,6,21.7,7.8,14,18,cosmetic +7767405,17,6,23.2,5.6,17,18,cosmetic +7767901,13,6,23.4,7.2,15,16,cosmetic +7880571,8,6,21.8,5.5,2,24,cosmetic +7935904,19,6,22.4,5.4,25,32,cosmetic +7928194,6,6,25.5,6.4,6,33,cosmetic +7957543,13,6,21.7,7.8,15,18,cosmetic +7918104,16,6,24.7,8.7,11,14,cosmetic +7973329,7,6,25.3,8.7,2,8,cosmetic +8028161,8,6,23.8,7.4,7,17,cosmetic +8011920,10,6,22.8,5.9,10,13,cosmetic +8107000,11,6,24.2,3.1,4,29,cosmetic +8181840,11,6,21.9,5.5,4,17,cosmetic +8248942,13,6,24.61,8.7,11,15,cosmetic +8219240,17,6,21.7,5.5,14,24,cosmetic +8232626,17,6,21.7,5.5,16,24,cosmetic +8250000,7,6,22.5,8,17,23,cosmetic +8358871,15,6,21.8,5.5,14,14,cosmetic +8349993,13,6,28.4,9,10,17,cosmetic +8310176,6,6,28,9,2,15,cosmetic +8381682,13,6,25.47,8.7,14,15,cosmetic +8475036,6,6,20,5.1,24,33,cosmetic +8427440,7,6,20.2,5.2,23,33,cosmetic +8458060,7,6,22.1,3.9,2,6,cosmetic +8400000,8,6,22.3,8.5,12,32,cosmetic +8527123,7,6,20,5.1,30,33,cosmetic +8516090,16,6,24.3,5.8,21,32,cosmetic +8541585,8,6,27.9,8.9,5,32,cosmetic +8500000,9,6,20.01,8.3,20,33,cosmetic +8645898,6,6,20.2,5.2,7,33,cosmetic +8558756,5,6,23.7,5,16,25,cosmetic +8645300,26,6,25.5,6.4,7,33,cosmetic +8676581,5,6,24,7.1,13,16,cosmetic +8740507,10,6,21.6,5.4,20,33,cosmetic +8723765,9,6,27.9,3.5,9,10,cosmetic +11000000,17,1,37.5,8,3,17,cosmetic +10800000,13,1,33.8,12,15,18,without +11950000,12,1,42,8.5,11,14,euro +12500000,15,1,39.8,9,1,16,cosmetic +38400000,10,1,54.1,12.7,13,14,without +8500000,35,1,36.7,12,4,24,cosmetic +11000000,10,1,35,11.5,13,26,cosmetic +11700000,24,1,46.9,13.6,8,14,euro +13200000,15,1,46.1,13,11,12,without +11500000,23,1,38.9,9,12,12,euro +8500000,36,1,43.8,12,9,22,cosmetic +9700000,10,1,35,10,19,20,euro +10500000,26,1,40.2,19,8,24,cosmetic +15900000,30,1,50.5,16.5,3,12,cosmetic +10950000,13,1,38.4,15.1,10,18,cosmetic +17190000,8,1,37.8,8.6,3,8,euro +9250000,17,1,36.6,9.8,20,23,cosmetic +25000000,6,1,41.6,13.7,21,24,euro +18200000,6,1,30,10,1,8,euro +8500000,26,1,40,11,8,21,cosmetic +14700000,23,1,46.7,8.2,7,12,euro +9200000,32,1,40.6,10,5,18,cosmetic +13600000,39,1,36.5,9,8,15,cosmetic +9980000,12,1,37.7,8.4,7,17,without +14000000,6,1,50.6,9.5,4,9,euro +19900000,5,1,45.2,15,15,42,without +23900000,7,1,47.4,14.5,2,13,euro +12400000,6,1,39,10,25,25,euro +12000000,12,1,36.7,10,3,12,cosmetic +7850000,15,1,32.9,8.3,2,3,without +13400000,10,1,36.4,10,8,14,cosmetic +11300000,8,1,38,8.1,17,17,euro +12600000,18,1,42,12.5,8,26,euro +12500000,29,1,35.5,13.8,3,17,euro +16500000,9,1,41.1,16.3,21,23,euro +12500000,15,1,34.3,10,9,16,cosmetic +13650000,13,1,46,12,20,32,euro +10150000,12,1,30.2,10,9,15,euro +11200000,25,1,34.5,11,16,25,cosmetic +13100000,10,1,38,10.3,11,26,cosmetic +11000000,30,1,50,15.2,11,12,without +8645000,16,1,34.7,9.2,5,9,cosmetic +15600000,9,1,42.4,16.5,15,23,euro +15990000,11,1,50.3,8,2,6,euro +14400000,12,1,38.5,10.1,10,22,cosmetic +7990000,20,1,37.8,10,5,15,cosmetic diff --git a/ReleaseGenerators/Files/Datasets/moscow_housing.meta b/ReleaseGenerators/Files/Datasets/moscow_housing.meta new file mode 100644 index 000000000..2bab1721c --- /dev/null +++ b/ReleaseGenerators/Files/Datasets/moscow_housing.meta @@ -0,0 +1,49 @@ +name = MoscowHousing + +task = regression + +features = rooms,area,kitchen_area,floor,floors_total,metro_minutes,renovation +target = price + +description.en = Apartment prices in Moscow with basic structural and location features. +description.ru = Цены на квартиры в Москве с основными характеристиками жилья и расположения. + +source = Real estate listings (aggregated dataset) +url = https://example.com/moscow-housing + +feature.price = float +feature.rooms = int +feature.area = float +feature.kitchen_area = float +feature.floor = int +feature.floors_total = int +feature.metro_minutes = float +feature.renovation = categorical + +feature.price.ru = цена квартиры +feature.rooms.ru = число комнат +feature.area.ru = площадь квартиры +feature.kitchen_area.ru = площадь кухни +feature.floor.ru = этаж +feature.floors_total.ru = этажей в доме +feature.metro_minutes.ru = минуты до метро +feature.renovation.ru = тип ремонта + +feature.price.en = apartment price +feature.rooms.en = number of rooms +feature.area.en = apartment area +feature.kitchen_area.en = kitchen area +feature.floor.en = floor +feature.floors_total.en = total floors in building +feature.metro_minutes.en = minutes to metro +feature.renovation.en = renovation type + +value.renovation.without.ru = без ремонта +value.renovation.cosmetic.ru = косметический +value.renovation.euro.ru = евроремонт +value.renovation.designer.ru = дизайнерский + +value.renovation.without.en = without renovation +value.renovation.cosmetic.en = cosmetic +value.renovation.euro.en = european renovation +value.renovation.designer.en = designer renovation \ No newline at end of file diff --git a/ReleaseGenerators/Files/Datasets/russian_cities.csv b/ReleaseGenerators/Files/Datasets/russian_cities.csv new file mode 100644 index 000000000..618589f7a --- /dev/null +++ b/ReleaseGenerators/Files/Datasets/russian_cities.csv @@ -0,0 +1,672 @@ +city,population,lat,lon,region_name,federal_district,foundation_year,area +Алейск,29,52.4922513,82.7793606,Алтайский край,Сибирский,1913,43.87 +Барнаул,632.4,53.3479968,83.7798064,Алтайский край,Сибирский,1730,322.01 +Белокуриха,15.2,51.996152,84.9839604,Алтайский край,Сибирский,1846,92.3 +Бийск,199.5,52.5393864,85.2138453,Алтайский край,Сибирский,1709,291.67 +Заринск,45.9,53.7063476,84.9315081,Алтайский край,Сибирский,1748,79.16 +Камень-на-Оби,40.5,53.7913974,81.3545053,Алтайский край,Сибирский,1751,48.79 +Рубцовск,141.6,51.5012067,81.2078695,Алтайский край,Сибирский,1892,84 +Яровое,18.1,52.9252146,78.5729775,Алтайский край,Сибирский,1943,44.38 +Белогорск,65.3,50.9213415,128.4739471,Амурская область,Дальневосточный,1860,117.6 +Благовещенск,226.4,50.290659,127.527198,Амурская область,Дальневосточный,1756,320.97 +Райчихинск,16.8,49.7941615,129.4112492,Амурская область,Дальневосточный,1932,388 +Тында,33.2,55.1546441,124.7468904,Амурская область,Дальневосточный,1907,34 +Шимановск,18.6,52.0051886,127.7005458,Амурская область,Дальневосточный,1910,50 +Архангельск,347,64.5394289,40.5169606,Архангельская область,Северо-Западный,1584,294.42 +Вельск,22.3,61.066,42.1032789,Архангельская область,Северо-Западный,1137,26.5 +Коряжма,35.7,61.2885948,47.1003015,Архангельская область,Северо-Западный,1535,50.11 +Котлас,62,61.2528972,46.633242,Архангельская область,Северо-Западный,1617,68.039 +Мирный,32.9,62.7645265,40.3360076,Архангельская область,Северо-Западный,1955,50.8 +Нарьян-Мар,25.1,67.6377777777778,53.0066666666667,Архангельская область,Северо-Западный,1929,19.53 +Няндома,19.1,61.6654674,40.2062947,Архангельская область,Северо-Западный,1898,21 +Северодвинск,182,64.5625385,39.8180934,Архангельская область,Северо-Западный,1936,120.51 +Астрахань,529.8,46.3655652,48.0559236,Астраханская область,Южный,1558,208.7 +Ахтубинск,36.5,48.2752034,46.1906462,Астраханская область,Южный,1768,17 +Камызяк,15.7,46.110579,48.07333,Астраханская область,Южный,1560,24.08 +Харабали,17.5,47.408999,47.2525345,Астраханская область,Южный,1789,36 +Белгород,394.1,50.5976472,36.5856652,Белгородская область,Центральный,1596,153.1 +Валуйки,34.2,50.2111207,38.0998772,Белгородская область,Центральный,1593,34 +Новый Оскол,18.5,50.7633747,37.8775484,Белгородская область,Центральный,1637,23.63 +Старый Оскол,223.9,51.2967101,37.8350182,Белгородская область,Центральный,1593,200.8 +Шебекино,40.9,50.4004883,36.8877889,Белгородская область,Центральный,1713,41 +Брянск,402.7,53.2419535,34.3652146,Брянская область,Центральный,985,186.73 +Карачев,17.2,53.1296524,34.9888727,Брянская область,Центральный,1146,15 +Клинцы,63,52.7529119,32.233911,Брянская область,Центральный,1707,64 +Новозыбков,39.5,52.537173,31.9357991,Брянская область,Центральный,1701,31.5 +Почеп,16.4,52.9154851,33.4744058,Брянская область,Центральный,1447,20 +Стародуб,18.2,52.5852257,32.760403,Брянская область,Центральный,1080,21.73 +Унеча,22.8,52.8461199,32.6757629,Брянская область,Центральный,1887,15 +Александров,57.9,56.391819,38.7111123,Владимирская область,Центральный,1300,26.16 +Владимир,356.9,56.1280804,40.4084376,Владимирская область,Центральный,990,137.14 +Вязники,34.5,56.29773,42.2687398,Владимирская область,Центральный,1608,40.75 +Гусь-Хрустальный,52.8,55.6198751,40.6579929,Владимирская область,Центральный,1756,43 +Киржач,26.7,56.1486863,38.8635701,Владимирская область,Центральный,1332,30.03 +Ковров,135.7,56.3554349,41.3170576,Владимирская область,Центральный,1100,57.4 +Кольчугино,42,56.3327254,39.391336,Владимирская область,Центральный,1871,31 +Муром,107,55.5630311,42.0231362,Владимирская область,Центральный,862,43.78 +Радужный,18.3,55.9960277,40.3321855,Владимирская область,Центральный,1971,113.2 +Собинка,17.3,55.9939169,40.0179653,Владимирская область,Центральный,1858,19.68 +Юрьев-Польский,18.1,56.4937757,39.6680539,Владимирская область,Центральный,1152,10 +Волгоград,1009,48.7070042,44.5170339,Волгоградская область,Южный,1589,859.353 +Волжский,323.9,48.7978209,44.7462538,Волгоградская область,Южный,1951,150 +Жирновск,15.5,50.9768814,44.7858202,Волгоградская область,Южный,1700,27 +Калач-на-Дону,23.6,48.6889024,43.5306303,Волгоградская область,Южный,1708,18 +Камышин,109.9,50.0651529,45.3844202,Волгоградская область,Южный,1668,81 +Котельниково,20.4,47.6310259,43.1330872,Волгоградская область,Южный,1897,40 +Краснослободск,17.1,48.7068721,44.5630857,Волгоградская область,Южный,1870,84 +Суровикино,18.2,48.618917,42.8541163,Волгоградская область,Южный,1744,33.433 +Урюпинск,36.7,50.7903789,42.0288513,Волгоградская область,Южный,1618,39.32 +Фролово,36.2,49.7649148,43.6648641,Волгоградская область,Южный,1859,60 +Великий Устюг,31,60.7603913,46.3054414,Вологодская область,Северо-Западный,1147,17 +Вологда,310.3,59.2483905,39.8355662,Вологодская область,Северо-Западный,1147,116 +Череповец,314.8,59.1269212,37.9090497,Вологодская область,Северо-Западный,1777,131 +Борисоглебск,59.9,51.3655754,42.1008334,Воронежская область,Центральный,1698,55.26 +Бутурлиновка,24,50.8311818,40.5976923,Воронежская область,Центральный,1740,37.72 +Воронеж,1058.3,51.6593332,39.1969229,Воронежская область,Центральный,1586,596.51 +Лиски,53.5,50.9945626,39.5184909,Воронежская область,Центральный,1500,64.77 +Нововоронеж,31.5,51.3091524,39.2162843,Воронежская область,Центральный,1957,46.13 +Острогожск,31.8,50.8677905,39.0407746,Воронежская область,Центральный,1652,22 +Павловск,24.5,50.453455,40.136874,Воронежская область,Центральный,1709,8.22 +Поворино,16.7,51.1954419,42.2472726,Воронежская область,Центральный,1870,18 +Россошь,62.6,50.1701949,39.6226965,Воронежская область,Центральный,1600,59.06 +Семилуки,27,51.6951644,39.0190454,Воронежская область,Центральный,1615,14 +Биробиджан,71.8,48.7946446,132.9217207,Еврейская автономная область,Дальневосточный,1915,169.38 +Борзя,29.2,50.3876058,116.5234779,Забайкальский край,Дальневосточный,1899,184 +Краснокаменск,51.4,50.0929703,118.0323936,Забайкальский край,Дальневосточный,1967,73 +Петровск-Забайкальский,15.8,51.2748592,108.846681,Забайкальский край,Дальневосточный,1789,300 +Чита,351.8,52.0340142,113.4994,Забайкальский край,Дальневосточный,1653,539 +Вичуга,33.3,57.2044698,41.9132201,Ивановская область,Центральный,1925,30 +Иваново,404.6,56.9993792,40.9728272,Ивановская область,Центральный,1561,104.84 +Кинешма,81,57.4425463,42.168914,Ивановская область,Центральный,1429,49 +Кохма,30.2,56.9324606,41.0931657,Ивановская область,Центральный,1619,12.6 +Приволжск,15.3,57.3805743,41.2808565,Ивановская область,Центральный,1484,16 +Тейково,31.8,56.8542719,40.5353874,Ивановская область,Центральный,1600,26.8 +Шуя,57,56.8560234,41.3800939,Ивановская область,Центральный,1394,33.29 +Ангарск,224.6,52.544879,103.888543,Иркутская область,Сибирский,1948,294 +Братск,226.3,56.1513108,101.6340035,Иркутская область,Сибирский,1631,262.94 +Вихоревка,20.8,56.1208145,101.1702926,Иркутская область,Сибирский,1947,15 +Железногорск-Илимский,22.9,56.5847888,104.114275,Иркутская область,Сибирский,1948,102 +Иркутск,623.6,52.2864036,104.2807466,Иркутская область,Сибирский,1661,279.98 +Нижнеудинск,33.6,54.8968931,99.0314056,Иркутская область,Сибирский,1648,75 +Саянск,38.8,54.1107238,102.18015,Иркутская область,Сибирский,1970,83 +Тайшет,32.7,55.9405334,98.0030145,Иркутская область,Сибирский,1897,75.72 +Тулун,39.7,54.557162,100.5780603,Иркутская область,Сибирский,1735,128 +Усолье-Сибирское,76,52.7565808,103.6388109,Иркутская область,Сибирский,1669,74 +Усть-Илимск,80.4,57.9430504,102.7415734,Иркутская область,Сибирский,1966,206.285 +Усть-Кут,40.8,56.7928178,105.7757343,Иркутская область,Сибирский,1631,56 +Черемхово,50.1,53.1369095,103.0901268,Иркутская область,Сибирский,1772,113.99 +Нальчик,239.6,43.4845464,43.60713,Кабардино-Балкарская Республика,Северо-Кавказский,1724,67 +Нарткала,30.6,43.5578075,43.8575925,Кабардино-Балкарская Республика,Северо-Кавказский,1913,9.45 +Прохладный,58.2,43.7589602,44.0102409,Кабардино-Балкарская Республика,Северо-Кавказский,1765,35.08 +Тырныауз,20.6,43.3981585,42.9213582,Кабардино-Балкарская Республика,Северо-Кавказский,1934,61.8 +Чегем,18.4,43.5671525,43.5865792,Кабардино-Балкарская Республика,Северо-Кавказский,1822,77.85 +Балтийск,33.7,54.6513372,19.9140572,Калининградская область,Северо-Западный,1626,49.1 +Гурьевск,19.3,54.770638,20.6039767,Калининградская область,Северо-Западный,1262,90 +Гусев,28.5,54.5915455,22.1942445,Калининградская область,Северо-Западный,1500,16.25 +Зеленоградск,16.5,54.9600185,20.4753652,Калининградская область,Северо-Западный,1252,17 +Калининград,489.3,54.7074702,20.5073241,Калининградская область,Северо-Западный,1255,224.7 +Черняховск,35.4,54.6244751,21.7969062,Калининградская область,Северо-Западный,1336,58 +Калуга,332,54.5059848,36.2516245,Калужская область,Центральный,1371,168.8 +Киров,29.9,54.0790111,34.3076201,Калужская область,Центральный,1744,36.48 +Козельск,16.4,54.0347201,35.780768,Калужская область,Центральный,1100,10 +Людиново,37.3,53.8700828,34.4385915,Калужская область,Центральный,1626,43 +Обнинск,117.4,55.0943144,36.6121639,Калужская область,Центральный,1946,50.15 +Вилючинск,22.2,52.9302415,158.4057632,Камчатский край,Дальневосточный,1968,341.24 +Елизово,39.3,53.1830375,158.3883548,Камчатский край,Дальневосточный,1848,70 +Петропавловск-Камчатский,179.6,53.036908,158.6559254,Камчатский край,Дальневосточный,1740,362.14 +Карачаевск,20.8,43.7732525,41.9143472,Карачаево-Черкесская Республика,Северо-Кавказский,1926,11.84 +Черкесск,123.2,44.2269425,42.0466704,Карачаево-Черкесская Республика,Северо-Кавказский,1825,69.8 +Анжеро-Судженск,68.1,56.0786281,86.0201278,Кемеровская область - Кузбасс,Сибирский,1896,106.03 +Белово,71.2,54.4220968,86.3037373,Кемеровская область - Кузбасс,Сибирский,1726,92 +Гурьевск,22.4,54.2859263,85.9475985,Кемеровская область - Кузбасс,Сибирский,1816,90 +Калтан,20.4,53.5210919,87.2771636,Кемеровская область - Кузбасс,Сибирский,1946,32.36 +Кемерово,556.4,55.3910651,86.0467781,Кемеровская область - Кузбасс,Сибирский,1701,294.8 +Киселевск,86.6,54.0059999,86.6366116,Кемеровская область - Кузбасс,Сибирский,1917,160 +Ленинск-Кузнецкий,94.4,54.6674492,86.1797324,Кемеровская область - Кузбасс,Сибирский,1763,128 +Мариинск,37.9,56.2127383,87.7454924,Кемеровская область - Кузбасс,Сибирский,1698,57.2 +Междуреченск,96.3,53.6865289,88.0702754,Кемеровская область - Кузбасс,Сибирский,1946,332.56 +Мыски,40.8,53.7125695,87.8055646,Кемеровская область - Кузбасс,Сибирский,1826,108.7 +Новокузнецк,549.4,53.794315,87.2142745,Кемеровская область - Кузбасс,Сибирский,1618,424.27 +Осинники,41.7,53.5988055,87.3371272,Кемеровская область - Кузбасс,Сибирский,1926,81 +Полысаево,25.9,54.6055,86.2809208,Кемеровская область - Кузбасс,Сибирский,1952,49.295 +Прокопьевск,190.3,53.8604265,86.7183577,Кемеровская область - Кузбасс,Сибирский,1650,227.5 +Таштагол,22.9,52.759313,87.8476546,Кемеровская область - Кузбасс,Сибирский,1939,79 +Топки,27.8,55.2764707,85.6152619,Кемеровская область - Кузбасс,Сибирский,1914,51.7 +Юрга,80.8,55.7202694,84.8886399,Кемеровская область - Кузбасс,Сибирский,1886,44.8 +Вятские Поляны,31.9,56.2284817,51.0614855,Кировская область,Приволжский,1596,28.34 +Киров,518.3,58.6035264,49.6679304,Кировская область,Приволжский,1181,169.73 +Кирово-Чепецк,69.8,58.5559424,50.0316848,Кировская область,Приволжский,1935,53.36 +Котельнич,22.9,58.3034832,48.3475597,Кировская область,Приволжский,1143,29.2 +Омутнинск,21.6,58.6698913,52.1894564,Кировская область,Приволжский,1773,45 +Слободской,31.9,58.7311574,50.1669249,Кировская область,Приволжский,1505,49.6 +Яранск,15.5,57.3040326,47.8478737,Кировская область,Приволжский,1584,18 +Волгореченск,16.3,57.4424559,41.1594201,Костромская область,Центральный,1964,17.83 +Кострома,276.9,57.768,40.927,Костромская область,Центральный,1152,144.5 +Шарья,23.5,58.3760542,45.4062414,Костромская область,Центральный,1906,25.3 +Абинск,39.1,44.8679655,38.1618157,Краснодарский край,Южный,1863,22 +Анапа,88.9,44.8950433,37.3163282,Краснодарский край,Южный,1781,58.983 +Апшеронск,39.5,44.4584006,39.7299824,Краснодарский край,Южный,1863,28 +Армавир,189,45.0012149,41.1324168,Краснодарский край,Южный,1839,280 +Белореченск,51.6,44.7651876,39.8781494,Краснодарский край,Южный,1862,38.48 +Геленджик,76.8,44.5630007,38.0790852,Краснодарский край,Южный,1831,27.9 +Горячий Ключ,40.3,44.6344864,39.1354738,Краснодарский край,Южный,1868,20.77 +Гулькевичи,34.3,45.3605121,40.6918389,Краснодарский край,Южный,1875,20.84 +Ейск,83.1,46.71157,38.2763895,Краснодарский край,Южный,1848,144 +Краснодар,932.6,45.0401604,38.9759647,Краснодарский край,Южный,1793,339.31 +Крымск,57.9,44.9344404,37.9855795,Краснодарский край,Южный,1862,91.76 +Курганинск,48.2,44.8877025,40.5913245,Краснодарский край,Южный,1855,43.97 +Лабинск,59.3,44.6354363,40.7245341,Краснодарский край,Южный,1841,46.41 +Новокубанск,35.2,45.1038699,41.0475175,Краснодарский край,Южный,1867,27.97 +Новороссийск,275,44.7235026,37.7686135,Краснодарский край,Южный,1838,81.1 +Приморско-Ахтарск,32.3,46.0515432,38.1704875,Краснодарский край,Южный,1854,18.69 +Славянск-на-Кубани,67.9,45.2603626,38.1259774,Краснодарский край,Южный,1865,43.5 +Сочи,443.6,43.5854551,39.7231548,Краснодарский край,Южный,1838,176.77 +Темрюк,41.4,45.2610949,37.4454412,Краснодарский край,Южный,1860,20.1 +Тимашевск,50.8,45.615923,38.9351837,Краснодарский край,Южный,1794,30 +Тихорецк,57.1,45.8546345,40.1260267,Краснодарский край,Южный,1895,78.05 +Туапсе,61.2,44.1105335,39.0824904,Краснодарский край,Южный,1838,33.41 +Усть-Лабинск,39.5,45.2227359,39.6929577,Краснодарский край,Южный,1794,37.15 +Хадыженск,22.2,44.4122963,39.5320258,Краснодарский край,Южный,1864,17 +Ачинск,105.5,56.253907,90.4794397,Красноярский край,Сибирский,1683,103.21 +Боготол,19.6,56.2098468,89.5299336,Красноярский край,Сибирский,1893,62.81 +Дивногорск,29.2,55.9576784,92.3800687,Красноярский край,Сибирский,1956,56.6 +Дудинка,20.8,69.4031364,86.1907218,Красноярский край,Сибирский,1667,10.5 +Енисейск,17.8,58.4485261,92.1651083,Красноярский край,Сибирский,1619,66.41 +Железногорск (Красноярский край),82.6,56.2529035,93.532273,Красноярский край,Сибирский,1957,456.6 +Железногорск (Курская обл.),100.4,52.3380202,35.3516867,Курская область,Центральный,1950,112 +Зеленогорск,61.6,56.1131564,94.5888103,Красноярский край,Сибирский,1956,162 +Кодинск,15.7,58.6032644,99.1797962,Красноярский край,Сибирский,1977,27 +Красноярск,1093.8,56.0093879,92.8524806,Красноярский край,Сибирский,1628,379.5 +Лесосибирск,59.3,58.221681,92.5037872,Красноярский край,Сибирский,1975,277 +Минусинск,67.9,53.7104586,91.6872907,Красноярский край,Сибирский,1739,60.5 +Норильск,181.8,69.3489978,88.2009846,Красноярский край,Сибирский,1935,30.14 +Ужур,15.3,55.3141969,89.8333918,Красноярский край,Сибирский,1760,38.9 +Шарыпово,36.7,55.5389739,89.1801044,Красноярский край,Сибирский,1700,30 +Курган,312.4,55.4443883,65.3161963,Курганская область,Уральский,1679,394 +Шадринск,74.7,56.0870344,63.6297182,Курганская область,Уральский,1662,171.5324 +Курск,453,51.7303637,36.1925603,Курская область,Центральный,1032,208.2 +Курчатов,38.1,51.6604083,35.6572224,Курская область,Центральный,1968,56.5 +Льгов,18.1,51.6597148,35.2612491,Курская область,Центральный,1152,37.4 +Бокситогорск,15.1,59.4734797,33.845688,Ленинградская область,Северо-Западный,1929,13 +Волхов,44.3,59.9005958,32.3520756,Ленинградская область,Северо-Западный,1918,108 +Всеволожск,74.7,60.0191278,30.6456718,Ленинградская область,Северо-Западный,1892,62.264 +Выборг,75.4,60.7130801,28.7328336,Ленинградская область,Северо-Западный,1293,160.847 +Гатчина,91.7,59.565237,30.1282473,Ленинградская область,Северо-Западный,1499,28.75 +Кингисепп,45.2,59.3740435,28.6112444,Ленинградская область,Северо-Западный,1384,44.03 +Кириши,50.5,59.4742862,32.0624947,Ленинградская область,Северо-Западный,1693,63.34 +Кировск,26.9,59.8754216,30.981364,Ленинградская область,Северо-Западный,1931,84.9 +Кудрово,41.1,59.9075226,30.5121008,Ленинградская область,Северо-Западный,1926,5 +Луга,34.6,58.7374031,29.8465776,Ленинградская область,Северо-Западный,1777,13 +Мурино,64.9,60.044862,30.4571456,Ленинградская область,Северо-Западный,1676,13.36 +Пикалево,19.7,59.5132022,34.1772776,Ленинградская область,Северо-Западный,1927,38 +Подпорожье,16.7,60.9127549,34.1567664,Ленинградская область,Северо-Западный,1936,30 +Приозерск,17.8,61.0330896,30.1587851,Ленинградская область,Северо-Западный,1100,16.5 +Светогорск,15.2,61.1111282,28.8725865,Ленинградская область,Северо-Западный,1887,49.54 +Сертолово,56.6,60.1446932,30.2095918,Ленинградская область,Северо-Западный,1936,27.49 +Сланцы,32.2,59.1178185,28.0881475,Ленинградская область,Северо-Западный,1934,36 +Сосновый Бор,67.7,59.8772884,29.1291619,Ленинградская область,Северо-Западный,1958,88 +Тихвин,58.1,59.6272904,33.5072731,Ленинградская область,Северо-Западный,1383,25.4 +Грязи,46.7,52.4874097,39.9331142,Липецкая область,Центральный,1868,33 +Данков,18.7,53.2577411,39.1456184,Липецкая область,Центральный,1563,21 +Елец,102.3,52.6152411,38.5289342,Липецкая область,Центральный,1146,71 +Липецк,508.6,52.610249,39.5947883,Липецкая область,Центральный,1703,330.15 +Магадан,92.1,59.5681332,150.8084956,Магаданская область,Дальневосточный,1929,295 +Москва,12655.1,55.7540471,37.620405,Москва,Центральный,1147,2561.5 +Апрелевка,30.7,55.5276918,37.065143,Московская область,Центральный,1899,29 +Балашиха,507.4,55.796389,37.938283,Московская область,Центральный,1830,67.4848 +Бронницы,22.5,55.4255379,38.264145,Московская область,Центральный,1453,22.16 +Видное,77,55.551725,37.7061984,Московская область,Центральный,1949,16.1 +Волоколамск,18.4,56.0356499,35.9585112,Московская область,Центральный,1135,30 +Воскресенск,92.9,55.3071519,38.7027953,Московская область,Центральный,1862,38.78 +Голицыно,17.5,55.6190582,36.9856793,Московская область,Центральный,1872,20 +Дедовск,30.5,55.8703276,37.1245043,Московская область,Центральный,1911,7 +Дзержинский,56.4,55.6240869,37.8440276,Московская область,Центральный,1380,16 +Дмитров,68.8,56.3477457,37.526672,Московская область,Центральный,1154,26 +Долгопрудный,116,55.9385999,37.5101021,Московская область,Центральный,1931,30.52 +Дубна,75,56.741786,37.1757223,Московская область,Центральный,1956,63.15 +Егорьевск,72.8,55.3830113,39.0358317,Московская область,Центральный,1462,29 +Жуковский,107.6,55.599803,38.1224298,Московская область,Центральный,1935,47.29 +Зарайск,22.8,54.762456,38.8850978,Московская область,Центральный,1146,20.5 +Звенигород,22.3,55.7297089,36.8554029,Московская область,Центральный,1152,48.1 +Ивантеевка,81.3,55.9741665,37.9207539,Московская область,Центральный,1564,14.59 +Кашира,47.4,54.853337,38.1904392,Московская область,Центральный,1356,26.35 +Клин,79.7,56.3425605,36.7240032,Московская область,Центральный,1317,42 +Коломна,140.1,55.102814,38.7531002,Московская область,Центральный,1177,65.1 +Королев,225.9,55.9161773,37.8545415,Московская область,Центральный,1938,36 +Котельники,49,55.6597925,37.8631156,Московская область,Центральный,1600,14.24 +Красноармейск,26.3,56.105426,38.140838,Московская область,Центральный,1928,156.43 +Красногорск,175.6,55.8317203,37.3295266,Московская область,Центральный,1932,25.65 +Кубинка,20.1,55.5754955,36.6951995,Московская область,Центральный,1400,4 +Куровское,20.6,55.5792277,38.9207723,Московская область,Центральный,1646,9 +Ликино-Дулево,28.8,55.7078257,38.9578093,Московская область,Центральный,1930,14 +Лобня,90.2,56.0328881,37.4614035,Московская область,Центральный,1902,31.56 +Лосино-Петровский,25.4,55.8713214,38.200599,Московская область,Центральный,1708,9.16 +Луховицы,30.4,54.9652077,39.0260266,Московская область,Центральный,1594,16.1 +Лыткарино,59.2,55.5778163,37.9033507,Московская область,Центральный,1429,17.29 +Люберцы,205.3,55.7003989,37.9606238,Московская область,Центральный,1623,12.87 +Можайск,30.1,55.5069259,36.024043,Московская область,Центральный,1231,17.8 +Мытищи,235.5,55.9105782,37.7363579,Московская область,Центральный,1460,34.59 +Наро-Фоминск,64.9,55.386185,36.734484,Московская область,Центральный,1840,22 +Ногинск,104,55.8686133,38.4622104,Московская область,Центральный,1389,43.348 +Одинцово,135.5,55.6789455,37.263686,Московская область,Центральный,1470,19.6 +Озеры,24.7,54.8541006,38.5599196,Московская область,Центральный,1834,12 +Орехово-Зуево,118.3,55.8151421,38.9869822,Московская область,Центральный,1917,36.38 +Павловский Посад,63.6,55.7807244,38.6596983,Московская область,Центральный,1328,39 +Подольск,308.1,55.4389322,37.5703482,Московская область,Центральный,1559,40.39 +Протвино,35.4,54.8705984,37.2182749,Московская область,Центральный,1960,26.69 +Пушкино,107.6,56.0103638,37.8471403,Московская область,Центральный,1499,60.33 +Пущино,20.7,54.8324412,37.6210346,Московская область,Центральный,1956,18.23 +Раменское,121.9,55.5495132,38.2728914,Московская область,Центральный,1328,59.46 +Рошаль,20,55.6632776,39.8656147,Московская область,Центральный,1916,35 +Сергиев Посад,100.3,56.3062548,38.1502661,Московская область,Центральный,1337,50.4 +Серпухов,126.3,54.9226466,37.4033859,Московская область,Центральный,1339,32.1 +Солнечногорск,50.2,56.185102,36.977631,Московская область,Центральный,1700,21 +Старая Купавна,24.2,55.810648,38.175624,Московская область,Центральный,1300,10.62 +Ступино,65.9,54.886274,38.078228,Московская область,Центральный,1938,35.5 +Фрязино,59.5,55.9590588,38.0410235,Московская область,Центральный,1584,9 +Химки,259.6,55.888657,37.4303702,Московская область,Центральный,1850,109.8 +Хотьково,21.3,56.2516982,37.9396017,Московская область,Центральный,1308,11 +Черноголовка,21.3,56.0100707,38.3792047,Московская область,Центральный,1710,2.023 +Чехов,72.3,55.1508011,37.4533252,Московская область,Центральный,1175,23 +Шатура,32,55.5777427,39.5445712,Московская область,Центральный,1423,36 +Щелково,126.1,55.9233801,37.9783707,Московская область,Центральный,1500,52 +Электрогорск,22.7,55.8780241,38.7806752,Московская область,Центральный,1912,39.81 +Электросталь,156,55.7847291,38.4447045,Московская область,Центральный,1916,51.45 +Электроугли,21.3,55.7170877,38.2192563,Московская область,Центральный,1899,4.5 +Апатиты,54.7,67.5677761,33.4067929,Мурманская область,Северо-Западный,1926,37.5 +Кандалакша,30.4,67.1567974,32.4143218,Мурманская область,Северо-Западный,1517,31 +Кировск,26,67.6150424,33.663735,Мурманская область,Северо-Западный,1929,84.9 +Ковдор,16.3,67.5661417,30.4741941,Мурманская область,Северо-Западный,1953,8 +Мончегорск,41.2,67.9386153,32.9359719,Мурманская область,Северо-Западный,1937,36.5 +Мурманск,287.8,69.007721,33.0685865,Мурманская область,Северо-Западный,1916,168.14 +Оленегорск,20.3,68.1422058,33.2669407,Мурманская область,Северо-Западный,1949,38.8 +Полярный,17.5,69.1989583,33.4508591,Мурманская область,Северо-Западный,1896,358.05 +Североморск,53.6,69.0766801,33.4177759,Мурманская область,Северо-Западный,1896,60 +Арзамас,104,55.3945814,43.8408141,Нижегородская область,Приволжский,1552,34.32 +Балахна,48.5,56.495036,43.5758423,Нижегородская область,Приволжский,1474,48 +Бор,76.9,56.3565458,44.0646481,Нижегородская область,Приволжский,1300,147 +Выкса,53,55.3207727,42.1678834,Нижегородская область,Приволжский,1500,34 +Городец,29.7,56.6449218,43.4723104,Нижегородская область,Приволжский,1152,124.2 +Дзержинск,229,56.2376047,43.4599416,Нижегородская область,Приволжский,1606,421 +Кстово,67.8,56.1432084,44.1664198,Нижегородская область,Приволжский,1300,59 +Кулебаки,31.8,55.4296181,42.5125538,Нижегородская область,Приволжский,1677,14 +Лысково,21.1,56.0262359,45.035771,Нижегородская область,Приволжский,1410,12 +Нижний Новгород,1252.2,56.3240627,44.0053913,Нижегородская область,Приволжский,1221,410 +Павлово,56.7,55.9797564,43.0995042,Нижегородская область,Приволжский,1566,39 +Саров,96.1,54.9228268,43.3448089,Нижегородская область,Приволжский,1691,249 +Сергач,19.9,55.5201515,45.4813231,Нижегородская область,Приволжский,1649,20 +Шахунья,19.7,57.6764293,46.6129009,Нижегородская область,Приволжский,1912,18 +Боровичи,49,58.3840197,33.9175929,Новгородская область,Северо-Западный,1400,45 +Великий Новгород,224.9,58.5213846,31.2755394,Новгородская область,Северо-Западный,930,90.08 +Старая Русса,27.7,57.990737,31.3554897,Новгородская область,Северо-Западный,1167,18.54 +Барабинск,28.7,55.35146,78.3464506,Новосибирская область,Сибирский,1893,69.48 +Бердск,104.3,54.7582156,83.1070605,Новосибирская область,Сибирский,1716,67 +Болотное,15.5,55.6692421,84.3906889,Новосибирская область,Сибирский,1805,59 +Искитим,56,54.6267603,83.2951244,Новосибирская область,Сибирский,1717,29.9 +Куйбышев,43.5,55.4685094,78.3242048,Новосибирская область,Сибирский,1722,109.73 +Новосибирск,1625.6,55.028191,82.9211489,Новосибирская область,Сибирский,1893,502.7 +Обь,29.9,54.9945576,82.6937181,Новосибирская область,Сибирский,1934,22 +Татарск,23.4,55.2146167,75.9739914,Новосибирская область,Сибирский,1894,80 +Тогучин,20.8,55.2251631,84.4104118,Новосибирская область,Сибирский,1867,81 +Черепаново,19.1,54.2206476,83.3724521,Новосибирская область,Сибирский,1912,20 +Исилькуль,22.4,54.9096002,71.2816284,Омская область,Сибирский,1895,39 +Калачинск,22.6,55.0598155,74.5653644,Омская область,Сибирский,1952,24 +Омск,1154.5,54.9848566,73.3674517,Омская область,Сибирский,1716,566.9 +Абдулино,18.6,53.6778906,53.6472483,Оренбургская область,Приволжский,1795,47 +Бугуруслан,48.5,53.6523728,52.4326853,Оренбургская область,Приволжский,1748,76 +Бузулук,86.1,52.7881277,52.2624877,Оренбургская область,Приволжский,1736,54 +Гай,33.8,51.4649189,58.4436875,Оренбургская область,Приволжский,1959,62.14 +Кувандык,23.3,51.4783857,57.3612636,Оренбургская область,Приволжский,1800,42.06 +Медногорск,24.6,51.4037617,57.583163,Оренбургская область,Приволжский,1933,91.33 +Новотроицк,83.7,51.1964202,58.3018192,Оренбургская область,Приволжский,1920,352.34 +Оренбург,572.2,51.7875092,55.1018828,Оренбургская область,Приволжский,1743,258.57 +Орск,226.5,51.2294282,58.4752777,Оренбургская область,Приволжский,1735,621.33 +Соль-Илецк,26.9,51.1633736,54.9896726,Оренбургская область,Приволжский,1754,180 +Сорочинск,27.1,52.426685,53.1542745,Оренбургская область,Приволжский,1737,55.93 +Ясный,15.3,51.036838,59.874344,Оренбургская область,Приволжский,1961,67 +Ливны,47.2,52.4284558,37.6039506,Орловская область,Центральный,1586,32.1 +Мценск,36.6,53.2788438,36.5749105,Орловская область,Центральный,1146,20.81 +Орел,308.8,52.9671298,36.0696427,Орловская область,Центральный,1566,121 +Заречный,65.5,53.1960836,45.1689907,Пензенская область,Приволжский,1958,29 +Кузнецк,80.5,53.1130888,46.605092,Пензенская область,Приволжский,1699,42.08 +Нижний Ломов,20.5,53.5300905,43.6730217,Пензенская область,Приволжский,1636,14 +Пенза,520.3,53.1753314,45.0348625,Пензенская область,Приволжский,1663,304.56 +Сердобск,30.9,52.4697595,44.2122414,Пензенская область,Приволжский,1698,42 +Березники,139.2,59.4079923,56.8039427,Пермский край,Приволжский,1623,431.1 +Губаха,19.1,58.8371721,57.554533,Пермский край,Приволжский,1755,58 +Краснокамск,52.5,58.0822065,55.7479936,Пермский край,Приволжский,1929,95.8 +Кудымкар,30.4,59.0167925,54.6572508,Пермский край,Приволжский,1579,25.3 +Кунгур,64.9,57.4283296,56.9438656,Пермский край,Приволжский,1663,68.7 +Лысьва,60.3,58.0995875,57.8086825,Пермский край,Приволжский,1785,26.44 +Пермь,1055.4,58.0102583,56.2342034,Пермский край,Приволжский,1723,800.5 +Соликамск,92.6,59.6482998,56.771009,Пермский край,Приволжский,1430,166.55 +Чайковский,82.4,56.7781501,54.1477965,Пермский край,Приволжский,1955,64 +Чернушка,33.1,56.5160768,56.0763049,Пермский край,Приволжский,1854,28 +Чусовой,43.7,58.2974596,57.8193615,Пермский край,Приволжский,1878,58 +Арсеньев,52.2,44.1622031,133.2696209,Приморский край,Дальневосточный,1902,39.37 +Артем,105.7,43.3501675,132.1596175,Приморский край,Дальневосточный,1908,122.58 +Большой Камень,39.1,43.1111742,132.3480082,Приморский край,Дальневосточный,1947,119.8 +Владивосток,606.6,43.1164904,131.8823937,Приморский край,Дальневосточный,1860,325.99 +Дальнегорск,34.1,44.5539457,135.5662716,Приморский край,Дальневосточный,1897,31 +Дальнереченск,25.1,45.9308483,133.7316907,Приморский край,Дальневосточный,1859,108.49 +Лесозаводск,35.1,45.4780092,133.4186199,Приморский край,Дальневосточный,1932,71 +Находка,145.2,42.8239372,132.8927361,Приморский край,Дальневосточный,1864,325.9 +Партизанск,36.6,43.1280578,133.1264567,Приморский край,Дальневосточный,1896,126 +Уссурийск,173.6,43.7971818,131.9518229,Приморский край,Дальневосточный,1866,173 +Фокино,22.9,42.9706317,132.4110196,Приморский край,Дальневосточный,1899,24 +Великие Луки,90.9,56.332208,30.5508641,Псковская область,Северо-Западный,1166,60.08 +Псков,210.3,57.8194415,28.3317198,Псковская область,Северо-Западный,903,95 +Майкоп,141.5,44.6098268,40.1006606,Республика Адыгея,Южный,1857,69.64 +Горно-Алтайск,64.5,51.9581028,85.9603235,Республика Алтай,Сибирский,1830,95.5 +Баймак,17.2,52.5912896,58.3110998,Республика Башкортостан,Приволжский,1850,139 +Белебей,59.2,54.1034582,54.1113129,Республика Башкортостан,Приволжский,1757,34.26 +Белорецк,64.9,53.9676488,58.4100419,Республика Башкортостан,Приволжский,1762,41 +Бирск,46.5,55.4155753,55.5582214,Республика Башкортостан,Приволжский,1663,77.31 +Благовещенск,34.8,55.0499867,55.9553186,Республика Башкортостан,Приволжский,1856,65 +Ишимбай,64.4,53.4545764,56.0438751,Республика Башкортостан,Приволжский,1932,103.47 +Кумертау,59.5,52.7564939,55.7970197,Республика Башкортостан,Приволжский,1947,170 +Мелеуз,56.7,52.9589532,55.9282838,Республика Башкортостан,Приволжский,1766,32 +Нефтекамск,131.1,56.088377,54.2483061,Республика Башкортостан,Приволжский,1957,147.25 +Салават,150.5,53.3616974,55.9245224,Республика Башкортостан,Приволжский,1948,106.23 +Сибай,60.8,52.7204651,58.6663783,Республика Башкортостан,Приволжский,1925,157.4 +Стерлитамак,276.4,53.6300864,55.9317089,Республика Башкортостан,Приволжский,1766,108.52 +Туймазы,68.2,54.5999224,53.6950623,Республика Башкортостан,Приволжский,1912,43 +Уфа,1128.8,54.734944,55.9578468,Республика Башкортостан,Приволжский,1574,708 +Учалы,37.9,54.3067375,59.4125461,Республика Башкортостан,Приволжский,1750,56 +Янаул,25.1,56.2650146,54.929907,Республика Башкортостан,Приволжский,1730,26 +Кяхта,20.3,50.346543,106.4533516,Республика Бурятия,Дальневосточный,1727,28 +Северобайкальск,23.2,55.635614,109.3361505,Республика Бурятия,Дальневосточный,1974,110.54 +Улан-Удэ,439.1,51.8335853,107.5842223,Республика Бурятия,Дальневосточный,1666,365.7 +Буйнакск,65.7,42.8214424,47.1165263,Республика Дагестан,Северо-Кавказский,1834,21 +Дагестанские Огни,29.7,42.1152296,48.1939354,Республика Дагестан,Северо-Кавказский,1914,9.27 +Дербент,125.8,42.058966,48.2907452,Республика Дагестан,Северо-Кавказский,438,69.63 +Избербаш,60.5,42.5650962,47.8710051,Республика Дагестан,Северо-Кавказский,1932,22 +Каспийск,124,42.8916007,47.6367066,Республика Дагестан,Северо-Кавказский,1932,36.92 +Кизилюрт,37.7,43.203825,46.8729636,Республика Дагестан,Северо-Кавказский,1927,50 +Кизляр,49.4,43.8484083,46.7233699,Республика Дагестан,Северо-Кавказский,1735,32.31 +Махачкала,603.5,42.9849159,47.5047181,Республика Дагестан,Северо-Кавказский,1844,592.72 +Хасавюрт,145.1,43.2504665,46.5851292,Республика Дагестан,Северо-Кавказский,1846,40 +Карабулак,42.7,43.3055248,44.9094582,Республика Ингушетия,Северо-Кавказский,1859,83.61 +Малгобек,38.6,43.5096646,44.5901963,Республика Ингушетия,Северо-Кавказский,1934,100.79 +Назрань,122.3,43.2257841,44.7645779,Республика Ингушетия,Северо-Кавказский,1781,118.28 +Сунжа,66.3,43.3204196,45.0476331,Республика Ингушетия,Северо-Кавказский,1845,23 +Элиста,103.1,46.3083344,44.2702088,Республика Калмыкия,Южный,1865,92.36 +Кондопога,29.2,62.2059817,34.2682122,Республика Карелия,Северо-Западный,1563,11 +Костомукша,29.6,64.5889398,30.6016832,Республика Карелия,Северо-Западный,1977,4 +Петрозаводск,281,61.7891264,34.3596434,Республика Карелия,Северо-Западный,1703,113.26 +Сегежа,26,63.7437572,34.3126982,Республика Карелия,Северо-Западный,1914,19 +Сортавала,18.8,61.703367,30.6916998,Республика Карелия,Северо-Западный,1468,10 +Воркута,52.8,67.4974082,64.061034,Республика Коми,Северо-Западный,1936,29.73 +Печора,38.2,65.148584,57.2239777,Республика Коми,Северо-Западный,1940,28.9 +Сосногорск,26,63.5989841,53.876293,Республика Коми,Северо-Западный,1939,24.9 +Сыктывкар,244.4,61.6686617,50.8358151,Республика Коми,Северо-Западный,1780,152 +Ухта,93.7,63.5565514,53.7014239,Республика Коми,Северо-Западный,1929,320 +Алушта,30,44.6764304,34.4100624,Республика Крым,Южный,500,6.983 +Армянск,21.4,46.1059307,33.6910012,Республика Крым,Южный,1736,16.2 +Бахчисарай,26.7,44.7514769,33.8752176,Республика Крым,Южный,1503,29.285 +Джанкой,38,45.7092306,34.3883372,Республика Крым,Южный,1842,26 +Евпатория,108.2,45.190629,33.367634,Республика Крым,Южный,0,43.1338 +Керчь,151.5,45.3562627,36.4674513,Республика Крым,Южный,0,109.161 +Красноперекопск,24.9,45.9537576,33.7921939,Республика Крым,Южный,1932,22.4198 +Саки,24.7,45.1341997,33.6033383,Республика Крым,Южный,1952,28.7 +Симферополь,342,44.9482948,34.1001151,Республика Крым,Южный,1784,107.4 +Судак,16.5,44.8504679,34.9762034,Республика Крым,Южный,212,23.4 +Феодосия,68,45.0318393,35.3824259,Республика Крым,Южный,0,33.301179 +Ялта,79,44.4953612,34.166308,Республика Крым,Южный,1154,15.6323 +Волжск,53.2,55.8623156,48.3715083,Республика Марий Эл,Приволжский,1933,39.47 +Йошкар-Ола,274.7,56.6343662,47.8999706,Республика Марий Эл,Приволжский,1584,100.39 +Козьмодемьянск,20.1,56.3334036,46.546675,Республика Марий Эл,Приволжский,1583,13.41 +Рузаевка,44.3,54.0581967,44.9490466,Республика Мордовия,Приволжский,1631,26.5 +Саранск,320.6,54.1809332,45.1862632,Республика Мордовия,Приволжский,1641,81.478 +Алдан,20.6,58.6094283,125.3817188,Республика Саха (Якутия),Дальневосточный,1924,33 +Ленск,23.2,60.7276196,114.9548255,Республика Саха (Якутия),Дальневосточный,1663,49 +Мирный,35.4,62.536232,113.9667728,Республика Саха (Якутия),Дальневосточный,1955,50.8 +Нерюнгри,57.9,56.6599953,124.7202403,Республика Саха (Якутия),Дальневосточный,1975,98.9 +Якутск,323,62.0281405,129.7325887,Республика Саха (Якутия),Дальневосточный,1632,122 +Алагир,19.7,43.0417684,44.2199715,Республика Северная Осетия – Алания,Северо-Кавказский,1850,26.76 +Беслан,37.4,43.1937529,44.5338707,Республика Северная Осетия – Алания,Северо-Кавказский,1847,23.17 +Владикавказ,303.6,43.020588,44.6819182,Республика Северная Осетия – Алания,Северо-Кавказский,1784,291.61 +Моздок,42,43.7471342,44.6569607,Республика Северная Осетия – Алания,Северо-Кавказский,1763,17.5 +Агрыз,19.7,56.5232864,52.9943775,Республика Татарстан,Приволжский,1646,8.6 +Азнакаево,34.2,54.8598642,53.0745527,Республика Татарстан,Приволжский,1762,15 +Альметьевск,158.4,54.9014619,52.2970467,Республика Татарстан,Приволжский,1700,118 +Арск,20.4,56.0912567,49.877067,Республика Татарстан,Приволжский,1200,19.27 +Бавлы,22.2,54.4062891,53.2458065,Республика Татарстан,Приволжский,1658,18.2 +Бугульма,82.5,54.5363495,52.7895849,Республика Татарстан,Приволжский,1736,27.87 +Елабуга,74,55.7567107,52.0543794,Республика Татарстан,Приволжский,1000,41.1 +Заинск,39.6,55.299053,52.0062972,Республика Татарстан,Приволжский,1656,25 +Зеленодольск,100,55.8466651,48.5010954,Республика Татарстан,Приволжский,1865,37.73 +Казань,1257.4,55.7943584,49.1114975,Республика Татарстан,Приволжский,1005,588.9844 +Лениногорск,61.7,54.5967034,52.4431906,Республика Татарстан,Приволжский,1795,24.65 +Мамадыш,15.9,55.7150413,51.4129016,Республика Татарстан,Приволжский,1391,15 +Менделеевск,22.1,55.895169,52.3143347,Республика Татарстан,Приволжский,1868,30.65 +Мензелинск,16.9,55.7270698,53.1003968,Республика Татарстан,Приволжский,1586,10 +Набережные Челны,533.8,55.7434619,52.3959165,Республика Татарстан,Приволжский,1626,161 +Нижнекамск,240,55.6313609,51.8144669,Республика Татарстан,Приволжский,1961,116 +Нурлат,32,54.4281461,50.8049337,Республика Татарстан,Приволжский,1905,22.6 +Чистополь,59.4,55.3699139,50.6285784,Республика Татарстан,Приволжский,1700,19.24 +Кызыл,119.4,51.7191047,94.4376882,Республика Тыва,Сибирский,1914,97.41 +Абакан,186.8,53.7223325,91.4436721,Республика Хакасия,Сибирский,1734,112.38 +Саяногорск,46,53.1008083,91.4122454,Республика Хакасия,Сибирский,1830,19 +Черногорск,75.4,53.8259342,91.3260229,Республика Хакасия,Сибирский,1936,117 +Азов,80.4,47.1121589,39.4232555,Ростовская область,Южный,1067,67.5 +Аксай,46,47.2676314,39.8756872,Ростовская область,Южный,1570,67.5 +Батайск,127.9,47.1383299,39.7507179,Ростовская область,Южный,1769,77.68 +Белая Калитва,39.3,48.1769737,40.8033529,Ростовская область,Южный,1703,22.1 +Волгодонск,171.4,47.5165338,42.1984951,Ростовская область,Южный,1950,182.3 +Гуково,63.2,48.0448904,39.9484672,Ростовская область,Южный,1878,40 +Донецк,46.7,48.3350706,39.945891,Ростовская область,Южный,1681,112 +Зверево,18,48.0435487,40.1265822,Ростовская область,Южный,1819,31.21 +Зерноград,24.1,46.8495958,40.312837,Ростовская область,Южный,1929,12.36 +Каменск-Шахтинский,87.8,48.3205326,40.2689583,Ростовская область,Южный,1671,159.99 +Константиновск,16.9,47.5773717,41.0967445,Ростовская область,Южный,1593,298 +Красный Сулин,37.6,47.8831311,40.0781298,Ростовская область,Южный,1797,93.6 +Миллерово,34.5,48.9260077,40.3984087,Ростовская область,Южный,1786,44.75 +Морозовск,24.6,48.3511807,41.8309225,Ростовская область,Южный,1910,26 +Новочеркасск,168,47.411919,40.1042098,Ростовская область,Южный,1805,128 +Новошахтинск,106.6,47.757773,39.9363697,Ростовская область,Южный,1910,136 +Ростов-на-Дону,1137.9,47.2224566,39.718803,Ростовская область,Южный,1749,348.5 +Сальск,56.8,46.4752095,41.5410415,Ростовская область,Южный,1812,43.883 +Семикаракорск,21.9,47.5177337,40.8114167,Ростовская область,Южный,1672,140 +Таганрог,248.6,47.2094917,38.9350989,Ростовская область,Южный,1698,95 +Шахты,230.3,47.7085287,40.2159846,Ростовская область,Южный,1805,158.2 +Касимов,29.2,54.9373475,41.3913211,Рязанская область,Центральный,1152,31.7 +Новомичуринск,16.5,54.0376572,39.74654,Рязанская область,Центральный,1968,26 +Рыбное,20.7,54.7256164,39.5134398,Рязанская область,Центральный,1597,15.36 +Ряжск,20.9,53.7067584,40.0522274,Рязанская область,Центральный,1502,21 +Рязань,539.3,54.6254445,39.7358609,Рязанская область,Центральный,1095,224.163 +Сасово,23.8,54.3508885,41.9117422,Рязанская область,Центральный,1642,23.79 +Скопин,25.7,53.823543,39.5492421,Рязанская область,Центральный,1676,30.9 +Жигулевск,51.6,53.4011981,49.4945176,Самарская область,Приволжский,1949,60.8 +Кинель,35.8,53.2210298,50.6343776,Самарская область,Приволжский,1837,36.75 +Новокуйбышевск,100.5,53.0994565,49.9477382,Самарская область,Приволжский,1946,86 +Отрадный,47.1,53.3800848,51.3438605,Самарская область,Приволжский,1920,10 +Похвистнево,27.9,53.6498299,52.1235156,Самарская область,Приволжский,1888,651.7829 +Самара,1156.7,53.1950306,50.1069518,Самарская область,Приволжский,1586,541.4 +Сызрань,167.1,53.1558674,48.4744629,Самарская область,Приволжский,1683,117 +Тольятти,699.4,53.5205348,49.3894028,Самарская область,Приволжский,1737,314.78 +Чапаевск,71.7,52.928961,49.8673269,Самарская область,Приволжский,1909,187.49 +Санкт-Петербург,5384.3,59.9391313,30.3159004,Санкт-Петербург,Северо-Западный,1703,1439 +Аткарск,24.1,51.8736062,45.0003247,Саратовская область,Приволжский,1700,10.54 +Балаково,187.5,52.0224231,47.7827765,Саратовская область,Приволжский,1762,79 +Балашов,75.8,51.5388697,43.1839757,Саратовская область,Приволжский,1600,72.9 +Вольск,61.9,52.0459865,47.3873595,Саратовская область,Приволжский,1780,148 +Ершов,18.8,51.3508505,48.2762226,Саратовская область,Приволжский,1893,21 +Калининск,15.6,51.4993591,44.4710435,Саратовская область,Приволжский,1680,24 +Красноармейск,22.3,51.023541,45.695044,Саратовская область,Приволжский,1928,156.43 +Новоузенск,15.1,50.455199,48.1413153,Саратовская область,Приволжский,1760,43 +Петровск,28.2,52.3094237,45.3851877,Саратовская область,Приволжский,1698,27 +Пугачев,40.6,52.0159921,48.7972223,Саратовская область,Приволжский,1764,79 +Ртищево,38.4,52.2616271,43.7842248,Саратовская область,Приволжский,1666,33 +Саратов,838,51.533557,46.034257,Саратовская область,Приволжский,1590,1490 +Энгельс,227,51.4854003,46.126722,Саратовская область,Приволжский,1747,113.77 +Оха,20,53.5867839,142.9412411,Сахалинская область,Дальневосточный,1908,20.98 +Поронайск,15.6,49.238866,143.1008333,Сахалинская область,Дальневосточный,1869,55 +Холмск,27.1,47.0408423,142.041688,Сахалинская область,Дальневосточный,1870,32 +Южно-Сахалинск,200.6,46.9591631,142.737976,Сахалинская область,Дальневосточный,1882,236.2 +Алапаевск,37.1,57.8475571,61.6693817,Свердловская область,Уральский,1639,95 +Арамиль,15.4,56.694632,60.8343125,Свердловская область,Уральский,1675,21.82 +Артемовский,30.1,57.3384177,61.8947443,Свердловская область,Уральский,1921,47 +Верхняя Пышма,72.7,56.9758903,60.5650383,Свердловская область,Уральский,1854,38.47 +Верхняя Салда,41.2,58.0465803,60.5560164,Свердловская область,Уральский,1778,72 +Дегтярск,15.6,56.7048206,60.079138,Свердловская область,Уральский,1914,19 +Екатеринбург,1493.7,56.8385216,60.6054911,Свердловская область,Уральский,1723,1110.69 +Заречный,27.9,56.8102931,61.3380029,Свердловская область,Уральский,1958,29 +Ивдель,15.4,60.6944496,60.4245069,Свердловская область,Уральский,1831,50 +Ирбит,36.5,57.6838362,63.057675,Свердловская область,Уральский,1631,64.23 +Каменск-Уральский,166.1,56.414962,61.9188674,Свердловская область,Уральский,1682,144.08 +Камышлов,25.8,56.8465034,62.7119766,Свердловская область,Уральский,1668,51.75 +Карпинск,25.9,59.7665925,60.0011703,Свердловская область,Уральский,1759,83 +Качканар,38.1,58.7051762,59.4839155,Свердловская область,Уральский,1957,317 +Кировград,18.7,57.4299433,60.0624051,Свердловская область,Уральский,1661,10 +Краснотурьинск,56.3,59.7636635,60.1934525,Свердловская область,Уральский,1758,309.5 +Красноуральск,22.5,58.348651,60.0408764,Свердловская область,Уральский,1925,21.7 +Красноуфимск,37.7,56.612387,57.7636637,Свердловская область,Уральский,1736,47 +Кушва,27.2,58.2826013,59.7645766,Свердловская область,Уральский,1735,50 +Невьянск,22.8,57.49131,60.2183429,Свердловская область,Уральский,1701,39.13 +Нижний Тагил,349,57.910126,59.9812853,Свердловская область,Уральский,1722,297.47 +Нижняя Тура,19.4,58.6309267,59.8520344,Свердловская область,Уральский,1754,42 +Новоуральск,80.4,57.2472567,60.0956714,Свердловская область,Уральский,1941,31.9 +Первоуральск,120.8,56.9080085,59.942926,Свердловская область,Уральский,1732,66 +Полевской,61,56.4956952,60.2365298,Свердловская область,Уральский,1718,57 +Ревда,61.5,56.7986319,59.9071591,Свердловская область,Уральский,1734,111 +Реж,36.8,57.3717477,61.3833842,Свердловская область,Уральский,1773,33 +Североуральск,25.6,60.1533109,59.9525245,Свердловская область,Уральский,1758,80 +Серов,95.9,59.6047724,60.5753882,Свердловская область,Уральский,1893,418 +Среднеуральск,23.7,56.9918901,60.4771018,Свердловская область,Уральский,1931,83.8 +Тавда,32.6,58.0434672,65.274217,Свердловская область,Уральский,1910,148 +Талица,15.6,57.0122687,63.7320757,Свердловская область,Уральский,1732,64 +Туринск,17.1,58.0393524,63.6981973,Свердловская область,Уральский,1600,28 +Севастополь,510,44.6167013,33.525355,Севастополь,Южный,1783,863.6 +Вязьма,52.3,55.2116983,34.2951663,Смоленская область,Центральный,1239,44 +Рославль,48.9,53.9449558,32.8480258,Смоленская область,Центральный,1137,43 +Смоленск,325.5,54.782635,32.045251,Смоленская область,Центральный,863,166.35 +Ярцево,42.7,55.0564732,32.6902302,Смоленская область,Центральный,1610,32 +Благодарный,30.4,45.0989782,43.4306455,Ставропольский край,Северо-Кавказский,1782,24 +Буденновск,61.3,44.7816067,44.1650339,Ставропольский край,Северо-Кавказский,1799,62.1 +Георгиевск,66.4,44.1485694,43.4739851,Ставропольский край,Северо-Кавказский,1777,25 +Ессентуки,113.1,44.0446186,42.8588653,Ставропольский край,Северо-Кавказский,1825,50 +Железноводск,24.9,44.1320568,43.0306461,Ставропольский край,Северо-Кавказский,1810,93.13 +Зеленокумск,34.1,44.4032668,43.8841877,Ставропольский край,Северо-Кавказский,1781,24 +Изобильный,37.8,45.3684296,41.708702,Ставропольский край,Северо-Кавказский,1895,28 +Ипатово,23.2,45.7181751,42.8970206,Ставропольский край,Северо-Кавказский,1860,16 +Кисловодск,128.8,43.9052044,42.7168721,Ставропольский край,Северо-Кавказский,1803,71.83 +Лермонтов,22.9,44.1054107,42.973135,Ставропольский край,Северо-Кавказский,1953,31 +Минеральные Воды,73.9,44.2087273,43.138408,Ставропольский край,Северо-Кавказский,1878,51.55 +Невинномысск,116.7,44.6226031,41.9476723,Ставропольский край,Северо-Кавказский,1825,98.5 +Нефтекумск,24.4,44.7544552,44.9865347,Ставропольский край,Северо-Кавказский,1958,40 +Новоалександровск,26.5,45.4932733,41.2153996,Ставропольский край,Северо-Кавказский,1804,21 +Новопавловск,26.1,43.9617097,43.6342865,Ставропольский край,Северо-Кавказский,1777,19 +Пятигорск,147.9,44.041091,43.0661553,Ставропольский край,Северо-Кавказский,1780,97 +Светлоград,35.4,45.328659,42.8565714,Ставропольский край,Северо-Кавказский,1786,41 +Ставрополь,450.7,45.044516,41.9689655,Ставропольский край,Северо-Кавказский,1777,171.7 +Кирсанов,16.1,52.6506335,42.7286445,Тамбовская область,Центральный,1702,10.9 +Котовск,28.9,52.5924489,41.5101237,Тамбовская область,Центральный,1940,17.3 +Мичуринск,90.7,52.8912389,40.5104443,Тамбовская область,Центральный,1635,78 +Моршанск,38,53.4436216,41.8115478,Тамбовская область,Центральный,1623,18.52 +Рассказово,42.7,52.6538833,41.874285,Тамбовская область,Центральный,1698,36.12 +Тамбов,292.1,52.7213154,41.452264,Тамбовская область,Центральный,1636,107.31 +Уварово,23.1,51.9767841,42.2529799,Тамбовская область,Центральный,1699,23 +Бежецк,20.4,57.7860089,36.6904983,Тверская область,Центральный,1137,17 +Бологое,20.5,57.8855767,34.0537771,Тверская область,Центральный,1495,120 +Вышний Волочек,45.5,57.568302,34.5404016,Тверская область,Центральный,1437,54 +Кимры,43.2,56.8733213,37.3556605,Тверская область,Центральный,1546,44 +Конаково,36.5,56.7275204,36.8012716,Тверская область,Центральный,1806,39.03 +Нелидово,18.1,56.2232566,32.7767459,Тверская область,Центральный,1900,20.5 +Осташков,15.4,57.1456744,33.1115372,Тверская область,Центральный,1300,13 +Ржев,57.5,56.262881,34.3291002,Тверская область,Центральный,1216,56.17 +Тверь,425.1,56.8586059,35.9116761,Тверская область,Центральный,1135,152.22 +Торжок,44.4,57.04133,34.9602344,Тверская область,Центральный,850,58.8 +Удомля,27.2,57.8787314,35.0167348,Тверская область,Центральный,1478,9 +Асино,24.4,56.9907085,86.1765257,Томская область,Сибирский,1896,90 +Колпашево,22.9,58.3114253,82.9025829,Томская область,Сибирский,1600,35 +Северск,106.5,56.6031285,84.8809926,Томская область,Сибирский,1949,485.65 +Стрежевой,40.8,60.732895,77.604122,Томская область,Сибирский,1966,31 +Томск,576.6,56.4845804,84.9481582,Томская область,Сибирский,1604,294.6 +Алексин,57.5,54.5083349,37.0478067,Тульская область,Центральный,1348,37.95 +Богородицк,30.4,53.7701014,38.1225152,Тульская область,Центральный,1663,27 +Кимовск,25.6,53.9698378,38.5380808,Тульская область,Центральный,1600,17 +Киреевск,26.3,53.9319555,37.9220351,Тульская область,Центральный,1762,11.49 +Плавск,15.7,53.7096415,37.2862352,Тульская область,Центральный,1563,8.64 +Суворов,17.2,54.1343585,36.4807419,Тульская область,Центральный,1949,25.6 +Тула,475.2,54.1920559,37.6153842,Тульская область,Центральный,1146,145.8 +Губкинский,29.2,64.4457594,76.4713274,Тюменская область,Уральский,1986,73 +Заводоуковск,26.5,56.5027463,66.5513613,Тюменская область,Уральский,1729,20.96 +Ишим,64.4,56.1104858,69.4795776,Тюменская область,Уральский,1687,46.1 +Когалым,67.7,62.2639527,74.4829794,Тюменская область,Уральский,1975,200 +Лабытнанги,26.3,66.6592841,66.3883009,Тюменская область,Уральский,1890,22 +Лангепас,44.6,61.2536939,75.1807763,Тюменская область,Уральский,1980,59.51 +Лянтор,41.2,61.6392863,72.179409,Тюменская область,Уральский,1932,63.07 +Мегион,46.6,61.0318712,76.1025878,Тюменская область,Уральский,1810,50.51 +Надым,44.8,65.5377966,72.5182736,Тюменская область,Уральский,1597,185 +Нефтеюганск,127.3,61.0882676,72.6164079,Тюменская область,Уральский,1961,154 +Нижневартовск,277.7,60.9396698,76.5696184,Тюменская область,Уральский,1909,270.59 +Новый Уренгой,118,66.0839433,76.6809681,Тюменская область,Уральский,1975,226.5 +Ноябрьск,106.9,63.2018039,75.4510581,Тюменская область,Уральский,1976,38.84 +Нягань,58.6,62.1454701,65.3946047,Тюменская область,Уральский,1965,814 +Покачи,18.1,61.7422169,75.5941517,Тюменская область,Уральский,1984,22.84 +Пыть-Ях,39.6,60.7585833,72.8365617,Тюменская область,Уральский,1968,80.4 +Радужный,43.7,62.1342888,77.4584094,Тюменская область,Уральский,1973,113.2 +Салехард,51,66.5492077,66.6085318,Тюменская область,Уральский,1595,84.5 +Сургут,380.6,61.2541083,73.3961587,Тюменская область,Уральский,1594,353.97 +Тобольск,98.9,58.2017299,68.2538558,Тюменская область,Уральский,1587,222 +Тюмень,807.3,57.1529744,65.5344099,Тюменская область,Уральский,1586,698.48 +Урай,40.5,60.1296954,64.8038508,Тюменская область,Уральский,1922,54.3 +Ханты-Мансийск,101.5,61.0023984,69.0184798,Тюменская область,Уральский,1582,337.76 +Югорск,38,61.3123568,63.3365484,Тюменская область,Уральский,1962,65.217 +Ялуторовск,40.3,56.6547289,66.3122992,Тюменская область,Уральский,1659,51.91 +Воткинск,97.2,57.0518149,53.9873096,Удмуртская Республика,Приволжский,1759,112.18 +Глазов,92.3,58.1359233,52.6635038,Удмуртская Республика,Приволжский,1678,68.27 +Ижевск,648.1,56.852738,53.2114896,Удмуртская Республика,Приволжский,1760,315.15 +Можга,49,56.4427774,52.2137886,Удмуртская Республика,Приволжский,1835,31 +Сарапул,95.4,56.4615767,53.8037657,Удмуртская Республика,Приволжский,1621,89 +Димитровград,113.5,54.2167926,49.6262585,Ульяновская область,Приволжский,1698,103.98 +Инза,17.2,53.8549647,46.3533459,Ульяновская область,Приволжский,1897,22 +Ульяновск,627.7,54.3079415,48.3748487,Ульяновская область,Приволжский,1648,316 +Амурск,38.9,50.2344147,136.8792444,Хабаровский край,Дальневосточный,1958,146.4 +Бикин,16.1,46.8185743,134.2550718,Хабаровский край,Дальневосточный,1895,85.74 +Комсомольск-на-Амуре,244.8,50.5498936,137.0079408,Хабаровский край,Дальневосточный,1860,325.1 +Николаевск-на-Амуре,17.9,53.1460657,140.7111367,Хабаровский край,Дальневосточный,1850,17 +Хабаровск,616.4,48.4647258,135.0598942,Хабаровский край,Дальневосточный,1858,389 +Аша,29.1,54.9906527,57.2783953,Челябинская область,Уральский,1898,52.4 +Бакал,18.7,54.9406399,58.8051698,Челябинская область,Уральский,1757,50 +Верхний Уфалей,26.7,56.0487158,60.2318886,Челябинская область,Уральский,1761,65 +Еманжелинск,28.2,54.7554548,61.3243477,Челябинская область,Уральский,1931,129.9 +Златоуст,163.9,55.1714905,59.6725549,Челябинская область,Уральский,1754,236.4 +Карталы,28.2,53.0536197,60.6478408,Челябинская область,Уральский,1810,86.13 +Касли,15.7,55.8868784,60.7421663,Челябинская область,Уральский,1747,39.76 +Катав-Ивановск,15.5,54.7521438,58.1983648,Челябинская область,Уральский,1755,24.7 +Копейск,147.6,55.116665,61.6179185,Челябинская область,Уральский,1907,236.71 +Коркино,34,54.8903147,61.4034576,Челябинская область,Уральский,1795,109 +Куса,16.7,55.3386053,59.4385778,Челябинская область,Уральский,1778,15 +Кыштым,36.2,55.7061276,60.5563781,Челябинская область,Уральский,1757,45.67 +Магнитогорск,413.3,53.4072153,58.9791437,Челябинская область,Уральский,1929,392.35 +Миасс,151.5,55.0456457,60.1077572,Челябинская область,Уральский,1773,111.9 +Озерск,78.4,55.763154,60.7076198,Челябинская область,Уральский,1946,657.3 +Сатка,41.3,55.0405288,59.0288975,Челябинская область,Уральский,1758,46.2 +Снежинск,51.8,56.0851495,60.7324914,Челябинская область,Уральский,1955,357 +Трехгорный,32.7,54.8178249,58.4464194,Челябинская область,Уральский,1952,162.49 +Троицк,73.4,54.0843745,61.5586831,Челябинская область,Уральский,1743,139.15 +Усть-Катав,21.8,54.9260812,58.152805,Челябинская область,Уральский,1758,27.45 +Чебаркуль,41.3,54.9818567,60.3773121,Челябинская область,Уральский,1736,71.13 +Челябинск,1196.7,55.1602624,61.4008078,Челябинская область,Уральский,1736,500.91 +Южноуральск,37.6,54.448927,61.2581158,Челябинская область,Уральский,1948,110.57 +Аргун,39.3,43.2916774,45.8723105,Чеченская Республика,Северо-Кавказский,1700,28 +Грозный,305.9,43.3180145,45.698291,Чеченская Республика,Северо-Кавказский,1818,324.16 +Гудермес,55.9,43.3519142,46.1035645,Чеченская Республика,Северо-Кавказский,1700,50 +Курчалой,26.6,43.2046547,46.0889364,Чеченская Республика,Северо-Кавказский,1860,38.15 +Урус-Мартан,62.7,43.120175,45.539276,Чеченская Республика,Северо-Кавказский,1708,25.13 +Шали,55.1,43.1488691,45.9009629,Чеченская Республика,Северо-Кавказский,1378,34.038 +Алатырь,33.8,54.8397989,46.5721997,Чувашская Республика,Приволжский,1552,41.7 +Канаш,44.8,55.507,47.4918273,Чувашская Республика,Приволжский,1588,18.5 +Новочебоксарск,127.2,56.1094977,47.4791113,Чувашская Республика,Приволжский,1960,51.14 +Чебоксары,497.6,56.1438298,47.2489782,Чувашская Республика,Приволжский,1469,251 +Шумерля,28.4,55.4962415,46.4182681,Чувашская Республика,Приволжский,1916,13.3 +Анадырь,15.8,64.7313924,177.5015421,Чукотский автономный округ,Дальневосточный,1889,20 +Гаврилов-Ям,16.5,57.3091058,39.8546444,Ярославская область,Центральный,1545,11 +Переславль-Залесский,37.9,56.7360544,38.8543617,Ярославская область,Центральный,1152,23.01 +Ростов Великий,30.5,57.2051315,39.4378622,Ярославская область,Центральный,862,32 +Рыбинск,184.6,58.0485495,38.8584119,Ярославская область,Центральный,1071,101.42 +Тутаев,39.8,57.8674993,39.5369627,Ярославская область,Центральный,1200,25.5 +Углич,31.8,57.5224249,38.3020044,Ярославская область,Центральный,1148,26.6 +Ярославль,608.4,57.6215477,39.8977411,Ярославская область,Центральный,1010,205.8 diff --git a/ReleaseGenerators/Files/Datasets/russian_cities.meta b/ReleaseGenerators/Files/Datasets/russian_cities.meta new file mode 100644 index 000000000..252f23e25 --- /dev/null +++ b/ReleaseGenerators/Files/Datasets/russian_cities.meta @@ -0,0 +1,56 @@ +name = RussianCities + +task = clustering + +features = population,lat,lon,region_name,federal_district,foundation_year,area + +description.en = Dataset of Russian cities with geographic, demographic, and historical features. Includes population, coordinates, region, foundation year, and area for urban analysis and modeling. +description.ru = Датасет городов России с географическими, демографическими и историческими характеристиками. Включает население, координаты, регион, год основания и площадь для анализа и моделирования. + +source = Wikipedia (cities, area, foundation year), aggregated and cleaned +url = https://ru.wikipedia.org/wiki/Список_городов_России + +feature.population = float +feature.lat = float +feature.lon = float +feature.region_name = categorical +feature.federal_district = categorical +feature.foundation_year = int +feature.area = float +feature.density = float + +feature.population.ru = население +feature.lat.ru = широта +feature.lon.ru = долгота +feature.region_name.ru = регион +feature.federal_district.ru = федеральный округ +feature.foundation_year.ru = год основания (нормализованный) +feature.area.ru = площадь города (км²) +feature.density.ru = плотность населения + +feature.population.en = population +feature.lat.en = latitude +feature.lon.en = longitude +feature.region_name.en = region +feature.federal_district.en = federal district +feature.foundation_year.en = foundation year (normalized) +feature.area.en = city area (km²) +feature.density.en = population density + +value.federal_district.central.ru = Центральный +value.federal_district.northwestern.ru = Северо-Западный +value.federal_district.southern.ru = Южный +value.federal_district.north_caucasian.ru = Северо-Кавказский +value.federal_district.volga.ru = Приволжский +value.federal_district.ural.ru = Уральский +value.federal_district.siberian.ru = Сибирский +value.federal_district.far_eastern.ru = Дальневосточный + +value.federal_district.central.en = Central +value.federal_district.northwestern.en = Northwestern +value.federal_district.southern.en = Southern +value.federal_district.north_caucasian.en = North Caucasian +value.federal_district.volga.en = Volga +value.federal_district.ural.en = Ural +value.federal_district.siberian.en = Siberian +value.federal_district.far_eastern.en = Far Eastern \ No newline at end of file diff --git a/ReleaseGenerators/Files/Datasets/titanic_ru.csv b/ReleaseGenerators/Files/Datasets/titanic_ru.csv new file mode 100644 index 000000000..efdfe240f --- /dev/null +++ b/ReleaseGenerators/Files/Datasets/titanic_ru.csv @@ -0,0 +1,892 @@ +Id,Имя,Выжил,Класс,Пол,Возраст,БратьяИСупруги,РодителиИДети,ЦенаБилета,ПортПосадки +1,Owen Harris Braund,0,3,муж,22,1,0,7.25,Саутгемптон +2,John Bradley Cumings,1,1,жен,38,1,0,71.2833,Шербур +3,Laina Heikkinen,1,3,жен,26,0,0,7.925,Саутгемптон +4,Jacques Heath Futrelle,1,1,жен,35,1,0,53.1,Саутгемптон +5,William Henry Allen,0,3,муж,35,0,0,8.05,Саутгемптон +6,James Moran,0,3,муж,,0,0,8.4583,Квинстаун +7,Timothy J McCarthy,0,1,муж,54,0,0,51.8625,Саутгемптон +8,Gosta Leonard Palsson,0,3,муж,2,3,1,21.075,Саутгемптон +9,Oscar W Johnson,1,3,жен,27,0,2,11.1333,Саутгемптон +10,Nicholas Adele Nasser,1,2,жен,14,1,0,30.0708,Шербур +11,Marguerite Rut Sandstrom,1,3,жен,4,1,1,16.7,Саутгемптон +12,Elizabeth Bonnell,1,1,жен,58,0,0,26.55,Саутгемптон +13,William Henry Saundercock,0,3,муж,20,0,0,8.05,Саутгемптон +14,Anders Johan Andersson,0,3,муж,39,1,5,31.275,Саутгемптон +15,Hulda Amanda Vestrom,0,3,жен,14,0,0,7.8542,Саутгемптон +16,Mary D Hewlett,1,2,жен,55,0,0,16,Саутгемптон +17,Eugene Rice,0,3,муж,2,4,1,29.125,Квинстаун +18,Charles Eugene Williams,1,2,муж,,0,0,13,Саутгемптон +19,Julius Emelia Vander Planke,0,3,жен,31,1,0,18,Саутгемптон +20,Fatima Masselmani,1,3,жен,,0,0,7.225,Шербур +21,Joseph J Fynney,0,2,муж,35,0,0,26,Саутгемптон +22,Lawrence Beesley,1,2,муж,34,0,0,13,Саутгемптон +23,Anna Annie McGowan,1,3,жен,15,0,0,8.0292,Квинстаун +24,William Thompson Sloper,1,1,муж,28,0,0,35.5,Саутгемптон +25,Torborg Danira Palsson,0,3,жен,8,3,1,21.075,Саутгемптон +26,Carl Oscar Asplund,1,3,жен,38,1,5,31.3875,Саутгемптон +27,Farred Chehab Emir,0,3,муж,,0,0,7.225,Шербур +28,Charles Alexander Fortune,0,1,муж,19,3,2,263,Саутгемптон +29,Ellen Nellie O'Dwyer,1,3,жен,,0,0,7.8792,Квинстаун +30,Lalio Todoroff,0,3,муж,,0,0,7.8958,Саутгемптон +31,Manuel E Uruchurtu,0,1,муж,40,0,0,27.7208,Шербур +32,William Augustus Spencer,1,1,жен,,1,0,146.5208,Шербур +33,Mary Agatha Glynn,1,3,жен,,0,0,7.75,Квинстаун +34,Edward H Wheadon,0,2,муж,66,0,0,10.5,Саутгемптон +35,Edgar Joseph Meyer,0,1,муж,28,1,0,82.1708,Шербур +36,Alexander Oskar Holverson,0,1,муж,42,1,0,52,Саутгемптон +37,Hanna Mamee,1,3,муж,,0,0,7.2292,Шербур +38,Ernest Charles Cann,0,3,муж,21,0,0,8.05,Саутгемптон +39,Augusta Maria Vander Planke,0,3,жен,18,2,0,18,Саутгемптон +40,Jamila Nicola-Yarred,1,3,жен,14,1,0,11.2417,Шербур +41,Johan Johanna Ahlin,0,3,жен,40,1,0,9.475,Саутгемптон +42,William John Turpin,0,2,жен,27,1,0,21,Саутгемптон +43,Theodor Kraeff,0,3,муж,,0,0,7.8958,Шербур +44,Simonne Marie Laroche,1,2,жен,3,1,2,41.5792,Шербур +45,Margaret Delia Devaney,1,3,жен,19,0,0,7.8792,Квинстаун +46,William John Rogers,0,3,муж,,0,0,8.05,Саутгемптон +47,Denis Lennon,0,3,муж,,1,0,15.5,Квинстаун +48,Bridget O'Driscoll,1,3,жен,,0,0,7.75,Квинстаун +49,Youssef Samaan,0,3,муж,,2,0,21.6792,Шербур +50,Josefine Arnold-Franchi,0,3,жен,18,1,0,17.8,Саутгемптон +51,Juha Niilo Panula,0,3,муж,7,4,1,39.6875,Саутгемптон +52,Richard Cater Nosworthy,0,3,муж,21,0,0,7.8,Саутгемптон +53,Henry Sleeper Harper,1,1,жен,49,1,0,76.7292,Шербур +54,Lizzie Elizabeth Faunthorpe,1,2,жен,29,1,0,26,Саутгемптон +55,Engelhart Cornelius Ostby,0,1,муж,65,0,1,61.9792,Шербур +56,Hugh Woolner,1,1,муж,,0,0,35.5,Саутгемптон +57,Emily Rugg,1,2,жен,21,0,0,10.5,Саутгемптон +58,Mansouer Novel,0,3,муж,28.5,0,0,7.2292,Шербур +59,Constance Mirium West,1,2,жен,5,1,2,27.75,Саутгемптон +60,William Frederick Goodwin,0,3,муж,11,5,2,46.9,Саутгемптон +61,Orsen Sirayanian,0,3,муж,22,0,0,7.2292,Шербур +62,Amelie Icard,1,1,жен,38,0,0,80, +63,Henry Birkhardt Harris,0,1,муж,45,1,0,83.475,Саутгемптон +64,Harald Skoog,0,3,муж,4,3,2,27.9,Саутгемптон +65,Albert A Stewart,0,1,муж,,0,0,27.7208,Шербур +66,Gerios Moubarek,1,3,муж,,1,1,15.2458,Шербур +67,Elizabeth Ramell Nye,1,2,жен,29,0,0,10.5,Саутгемптон +68,Ernest James Crease,0,3,муж,19,0,0,8.1583,Саутгемптон +69,Erna Alexandra Andersson,1,3,жен,17,4,2,7.925,Саутгемптон +70,Vincenz Kink,0,3,муж,26,2,0,8.6625,Саутгемптон +71,Stephen Curnow Jenkin,0,2,муж,32,0,0,10.5,Саутгемптон +72,Lillian Amy Goodwin,0,3,жен,16,5,2,46.9,Саутгемптон +73,Ambrose Jr Hood,0,2,муж,21,0,0,73.5,Саутгемптон +74,Apostolos Chronopoulos,0,3,муж,26,1,0,14.4542,Шербур +75,Lee Bing,1,3,муж,32,0,0,56.4958,Саутгемптон +76,Sigurd Hansen Moen,0,3,муж,25,0,0,7.65,Саутгемптон +77,Ivan Staneff,0,3,муж,,0,0,7.8958,Саутгемптон +78,Rahamin Haim Moutal,0,3,муж,,0,0,8.05,Саутгемптон +79,Alden Gates Caldwell,1,2,муж,0.83,0,2,29,Саутгемптон +80,Elizabeth Dowdell,1,3,жен,30,0,0,12.475,Саутгемптон +81,Achille Waelens,0,3,муж,22,0,0,9,Саутгемптон +82,Jan Baptist Sheerlinck,1,3,муж,29,0,0,9.5,Саутгемптон +83,Brigdet Delia McDermott,1,3,жен,,0,0,7.7875,Квинстаун +84,Francisco M Carrau,0,1,муж,28,0,0,47.1,Саутгемптон +85,Bertha Ilett,1,2,жен,17,0,0,10.5,Саутгемптон +86,Karl Alfred Backstrom,1,3,жен,33,3,0,15.85,Саутгемптон +87,William Neal Ford,0,3,муж,16,1,3,34.375,Саутгемптон +88,Selman Francis Slocovski,0,3,муж,,0,0,8.05,Саутгемптон +89,Mabel Helen Fortune,1,1,жен,23,3,2,263,Саутгемптон +90,Francesco Celotti,0,3,муж,24,0,0,8.05,Саутгемптон +91,Emil Christmann,0,3,муж,29,0,0,8.05,Саутгемптон +92,Paul Edvin Andreasson,0,3,муж,20,0,0,7.8542,Саутгемптон +93,Herbert Fuller Chaffee,0,1,муж,46,1,0,61.175,Саутгемптон +94,Bertram Frank Dean,0,3,муж,26,1,2,20.575,Саутгемптон +95,Daniel Coxon,0,3,муж,59,0,0,7.25,Саутгемптон +96,Charles Joseph Shorney,0,3,муж,,0,0,8.05,Саутгемптон +97,George B Goldschmidt,0,1,муж,71,0,0,34.6542,Шербур +98,William Bertram Greenfield,1,1,муж,23,0,1,63.3583,Шербур +99,John T Doling,1,2,жен,34,0,1,23,Саутгемптон +100,Sinai Kantor,0,2,муж,34,1,0,26,Саутгемптон +101,Matilda Petranec,0,3,жен,28,0,0,7.8958,Саутгемптон +102,Pastcho Pentcho Petroff,0,3,муж,,0,0,7.8958,Саутгемптон +103,Richard Frasar White,0,1,муж,21,0,1,77.2875,Саутгемптон +104,Gustaf Joel Johansson,0,3,муж,33,0,0,8.6542,Саутгемптон +105,Anders Vilhelm Gustafsson,0,3,муж,37,2,0,7.925,Саутгемптон +106,Stoytcho Mionoff,0,3,муж,28,0,0,7.8958,Саутгемптон +107,Anna Kristine Salkjelsvik,1,3,жен,21,0,0,7.65,Саутгемптон +108,Albert Johan Moss,1,3,муж,,0,0,7.775,Саутгемптон +109,Tido Rekic,0,3,муж,38,0,0,7.8958,Саутгемптон +110,Bertha Moran,1,3,жен,,1,0,24.15,Квинстаун +111,Walter Chamberlain Porter,0,1,муж,47,0,0,52,Саутгемптон +112,Hileni Zabour,0,3,жен,14.5,1,0,14.4542,Шербур +113,David John Barton,0,3,муж,22,0,0,8.05,Саутгемптон +114,Katriina Jussila,0,3,жен,20,1,0,9.825,Саутгемптон +115,Malake Attalah,0,3,жен,17,0,0,14.4583,Шербур +116,Edvard Pekoniemi,0,3,муж,21,0,0,7.925,Саутгемптон +117,Patrick Connors,0,3,муж,70.5,0,0,7.75,Квинстаун +118,William John Turpin,0,2,муж,29,1,0,21,Саутгемптон +119,Quigg Edmond Baxter,0,1,муж,24,0,1,247.5208,Шербур +120,Ellis Anna Andersson,0,3,жен,2,4,2,31.275,Саутгемптон +121,Stanley George Hickman,0,2,муж,21,2,0,73.5,Саутгемптон +122,Leonard Charles Moore,0,3,муж,,0,0,8.05,Саутгемптон +123,Nicholas Nasser,0,2,муж,32.5,1,0,30.0708,Шербур +124,Susan Webber,1,2,жен,32.5,0,0,13,Саутгемптон +125,Percival Wayland White,0,1,муж,54,0,1,77.2875,Саутгемптон +126,Elias Nicola-Yarred,1,3,муж,12,1,0,11.2417,Шербур +127,Martin McMahon,0,3,муж,,0,0,7.75,Квинстаун +128,Fridtjof Arne Madsen,1,3,муж,24,0,0,7.1417,Саутгемптон +129,Anna Peter,1,3,жен,,1,1,22.3583,Шербур +130,Johan Ekstrom,0,3,муж,45,0,0,6.975,Саутгемптон +131,Jozef Drazenoic,0,3,муж,33,0,0,7.8958,Шербур +132,Domingos Fernandeo Coelho,0,3,муж,20,0,0,7.05,Саутгемптон +133,Alexander A Robins,0,3,жен,47,1,0,14.5,Саутгемптон +134,Leopold Mathilde Weisz,1,2,жен,29,1,0,26,Саутгемптон +135,Samuel James Sobey,0,2,муж,25,0,0,13,Саутгемптон +136,Emile Richard,0,2,муж,23,0,0,15.0458,Шербур +137,Helen Monypeny Newsom,1,1,жен,19,0,2,26.2833,Саутгемптон +138,Jacques Heath Futrelle,0,1,муж,37,1,0,53.1,Саутгемптон +139,Olaf Elon Osen,0,3,муж,16,0,0,9.2167,Саутгемптон +140,Victor Giglio,0,1,муж,24,0,0,79.2,Шербур +141,Joseph Sultana Boulos,0,3,жен,,0,2,15.2458,Шербур +142,Anna Sofia Nysten,1,3,жен,22,0,0,7.75,Саутгемптон +143,Pekka Pietari Hakkarainen,1,3,жен,24,1,0,15.85,Саутгемптон +144,Jeremiah Burke,0,3,муж,19,0,0,6.75,Квинстаун +145,Edgardo Samuel Andrew,0,2,муж,18,0,0,11.5,Саутгемптон +146,Joseph Charles Nicholls,0,2,муж,19,1,1,36.75,Саутгемптон +147,August Edvard Andersson,1,3,муж,27,0,0,7.7958,Саутгемптон +148,Robina Maggie Ford,0,3,жен,9,2,2,34.375,Саутгемптон +149,Michel Louis Navratil,0,2,муж,36.5,0,2,26,Саутгемптон +150,Thomas Roussel Byles,0,2,муж,42,0,0,13,Саутгемптон +151,Robert James Bateman,0,2,муж,51,0,0,12.525,Саутгемптон +152,Thomas Edith Pears,1,1,жен,22,1,0,66.6,Саутгемптон +153,Alfonzo Meo,0,3,муж,55.5,0,0,8.05,Саутгемптон +154,Austin Blyler van Billiard,0,3,муж,40.5,0,2,14.5,Саутгемптон +155,Ole Martin Olsen,0,3,муж,,0,0,7.3125,Саутгемптон +156,Charles Duane Williams,0,1,муж,51,0,1,61.3792,Шербур +157,Katherine Katie Gilnagh,1,3,жен,16,0,0,7.7333,Квинстаун +158,Harry Corn,0,3,муж,30,0,0,8.05,Саутгемптон +159,Mile Smiljanic,0,3,муж,,0,0,8.6625,Саутгемптон +160,Thomas Henry Sage,0,3,муж,,8,2,69.55,Саутгемптон +161,John Hatfield Cribb,0,3,муж,44,0,1,16.1,Саутгемптон +162,James Elizabeth Watt,1,2,жен,40,0,0,15.75,Саутгемптон +163,John Viktor Bengtsson,0,3,муж,26,0,0,7.775,Саутгемптон +164,Jovo Calic,0,3,муж,17,0,0,8.6625,Саутгемптон +165,Eino Viljami Panula,0,3,муж,1,4,1,39.6875,Саутгемптон +166,Frank John Goldsmith,1,3,муж,9,0,2,20.525,Саутгемптон +167,Edith Martha Chibnall,1,1,жен,,0,1,55,Саутгемптон +168,William Anna Skoog,0,3,жен,45,1,4,27.9,Саутгемптон +169,John D Baumann,0,1,муж,,0,0,25.925,Саутгемптон +170,Lee Ling,0,3,муж,28,0,0,56.4958,Саутгемптон +171,Wyckoff Van der hoef,0,1,муж,61,0,0,33.5,Саутгемптон +172,Arthur Rice,0,3,муж,4,4,1,29.125,Квинстаун +173,Eleanor Ileen Johnson,1,3,жен,1,1,1,11.1333,Саутгемптон +174,Antti Wilhelm Sivola,0,3,муж,21,0,0,7.925,Саутгемптон +175,James Clinch Smith,0,1,муж,56,0,0,30.6958,Шербур +176,Klas Albin Klasen,0,3,муж,18,1,1,7.8542,Саутгемптон +177,Henry Forbes Lefebre,0,3,муж,,3,1,25.4667,Саутгемптон +178,Ann Elizabeth Isham,0,1,жен,50,0,0,28.7125,Шербур +179,Reginald Hale,0,2,муж,30,0,0,13,Саутгемптон +180,Lionel Leonard,0,3,муж,36,0,0,0,Саутгемптон +181,Constance Gladys Sage,0,3,жен,,8,2,69.55,Саутгемптон +182,Rene Pernot,0,2,муж,,0,0,15.05,Шербур +183,Clarence Gustaf Asplund,0,3,муж,9,4,2,31.3875,Саутгемптон +184,Richard F Becker,1,2,муж,1,2,1,39,Саутгемптон +185,Luise Kink-Heilmann,1,3,жен,4,0,2,22.025,Саутгемптон +186,Hugh Roscoe Rood,0,1,муж,,0,0,50,Саутгемптон +187,Thomas Johanna O'Brien,1,3,жен,,1,0,15.5,Квинстаун +188,Charles Hallace Romaine,1,1,муж,45,0,0,26.55,Саутгемптон +189,John Bourke,0,3,муж,40,1,1,15.5,Квинстаун +190,Stjepan Turcin,0,3,муж,36,0,0,7.8958,Саутгемптон +191,Rosa Pinsky,1,2,жен,32,0,0,13,Саутгемптон +192,William Carbines,0,2,муж,19,0,0,13,Саутгемптон +193,Carla Andersen-Jensen,1,3,жен,19,1,0,7.8542,Саутгемптон +194,Michel M Navratil,1,2,муж,3,1,1,26,Саутгемптон +195,James Joseph Brown,1,1,жен,44,0,0,27.7208,Шербур +196,Elise Lurette,1,1,жен,58,0,0,146.5208,Шербур +197,Robert Mernagh,0,3,муж,,0,0,7.75,Квинстаун +198,Karl Siegwart Olsen,0,3,муж,42,0,1,8.4042,Саутгемптон +199,Margaret Maggie Madigan,1,3,жен,,0,0,7.75,Квинстаун +200,Henriette Mrs Yrois,0,2,жен,24,0,0,13,Саутгемптон +201,Nestor Cyriel Vande Walle,0,3,муж,28,0,0,9.5,Саутгемптон +202,Frederick Sage,0,3,муж,,8,2,69.55,Саутгемптон +203,Jakob Alfred Johanson,0,3,муж,34,0,0,6.4958,Саутгемптон +204,Gerious Youseff,0,3,муж,45.5,0,0,7.225,Шербур +205,Gurshon Gus Cohen,1,3,муж,18,0,0,8.05,Саутгемптон +206,Telma Matilda Strom,0,3,жен,2,0,1,10.4625,Саутгемптон +207,Karl Alfred Backstrom,0,3,муж,32,1,0,15.85,Саутгемптон +208,Nassef Cassem Albimona,1,3,муж,26,0,0,18.7875,Шербур +209,Helen Ellen Carr,1,3,жен,16,0,0,7.75,Квинстаун +210,Henry Blank,1,1,муж,40,0,0,31,Шербур +211,Ahmed Ali,0,3,муж,24,0,0,7.05,Саутгемптон +212,Clear Annie Cameron,1,2,жен,35,0,0,21,Саутгемптон +213,John Henry Perkin,0,3,муж,22,0,0,7.25,Саутгемптон +214,Hans Kristensen Givard,0,2,муж,30,0,0,13,Саутгемптон +215,Philip Kiernan,0,3,муж,,1,0,7.75,Квинстаун +216,Madeleine Newell,1,1,жен,31,1,0,113.275,Шербур +217,Eliina Honkanen,1,3,жен,27,0,0,7.925,Саутгемптон +218,Sidney Samuel Jacobsohn,0,2,муж,42,1,0,27,Саутгемптон +219,Albina Bazzani,1,1,жен,32,0,0,76.2917,Шербур +220,Walter Harris,0,2,муж,30,0,0,10.5,Саутгемптон +221,Victor Francis Sunderland,1,3,муж,16,0,0,8.05,Саутгемптон +222,James H Bracken,0,2,муж,27,0,0,13,Саутгемптон +223,George Henry Green,0,3,муж,51,0,0,8.05,Саутгемптон +224,Christo Nenkoff,0,3,муж,,0,0,7.8958,Саутгемптон +225,Frederick Maxfield Hoyt,1,1,муж,38,1,0,90,Саутгемптон +226,Karl Ivar Berglund,0,3,муж,22,0,0,9.35,Саутгемптон +227,William John Mellors,1,2,муж,19,0,0,10.5,Саутгемптон +228,John Hall Lovell,0,3,муж,20.5,0,0,7.25,Саутгемптон +229,Arne Jonas Fahlstrom,0,2,муж,18,0,0,13,Саутгемптон +230,Mathilde Lefebre,0,3,жен,,3,1,25.4667,Саутгемптон +231,Henry Birkhardt Harris,1,1,жен,35,1,0,83.475,Саутгемптон +232,Bengt Edvin Larsson,0,3,муж,29,0,0,7.775,Саутгемптон +233,Ernst Adolf Sjostedt,0,2,муж,59,0,0,13.5,Саутгемптон +234,Lillian Gertrud Asplund,1,3,жен,5,4,2,31.3875,Саутгемптон +235,Robert William Leyson,0,2,муж,24,0,0,10.5,Саутгемптон +236,Alice Phoebe Harknett,0,3,жен,,0,0,7.55,Саутгемптон +237,Stephen Hold,0,2,муж,44,1,0,26,Саутгемптон +238,Marjorie Lottie Collyer,1,2,жен,8,0,2,26.25,Саутгемптон +239,Frederick William Pengelly,0,2,муж,19,0,0,10.5,Саутгемптон +240,George Henry Hunt,0,2,муж,33,0,0,12.275,Саутгемптон +241,Thamine Zabour,0,3,жен,,1,0,14.4542,Шербур +242,Katherine Kate Murphy,1,3,жен,,1,0,15.5,Квинстаун +243,Reginald Charles Coleridge,0,2,муж,29,0,0,10.5,Саутгемптон +244,Matti Alexanteri Maenpaa,0,3,муж,22,0,0,7.125,Саутгемптон +245,Sleiman Attalah,0,3,муж,30,0,0,7.225,Шербур +246,William Edward Minahan,0,1,муж,44,2,0,90,Квинстаун +247,Agda Thorilda Lindahl,0,3,жен,25,0,0,7.775,Саутгемптон +248,William Anna Hamalainen,1,2,жен,24,0,2,14.5,Саутгемптон +249,Richard Leonard Beckwith,1,1,муж,37,1,1,52.5542,Саутгемптон +250,Ernest Courtenay Carter,0,2,муж,54,1,0,26,Саутгемптон +251,James George Reed,0,3,муж,,0,0,7.25,Саутгемптон +252,Wilhelm Elna Strom,0,3,жен,29,1,1,10.4625,Саутгемптон +253,William Thomas Stead,0,1,муж,62,0,0,26.55,Саутгемптон +254,William Arthur Lobb,0,3,муж,30,1,0,16.1,Саутгемптон +255,Viktor Helena Rosblom,0,3,жен,41,0,2,20.2125,Саутгемптон +256,Darwis Hanne Touma,1,3,жен,29,0,2,15.2458,Шербур +257,Gertrude Maybelle Thorne,1,1,жен,,0,0,79.2,Шербур +258,Gladys Cherry,1,1,жен,30,0,0,86.5,Саутгемптон +259,Anna Ward,1,1,жен,35,0,0,512.3292,Шербур +260,Lutie Davis Parrish,1,2,жен,50,0,1,26,Саутгемптон +261,Thomas Smith,0,3,муж,,0,0,7.75,Квинстаун +262,Edvin Rojj Asplund,1,3,муж,3,4,2,31.3875,Саутгемптон +263,Emil Taussig,0,1,муж,52,1,1,79.65,Саутгемптон +264,William Harrison,0,1,муж,40,0,0,0,Саутгемптон +265,Delia Henry,0,3,жен,,0,0,7.75,Квинстаун +266,David Reeves,0,2,муж,36,0,0,10.5,Саутгемптон +267,Ernesti Arvid Panula,0,3,муж,16,4,1,39.6875,Саутгемптон +268,Ernst Ulrik Persson,1,3,муж,25,1,0,7.775,Саутгемптон +269,William Thompson Graham,1,1,жен,58,0,1,153.4625,Саутгемптон +270,Amelia Bissette,1,1,жен,35,0,0,135.6333,Саутгемптон +271,Alexander Cairns,0,1,муж,,0,0,31,Саутгемптон +272,William Henry Tornquist,1,3,муж,25,0,0,0,Саутгемптон +273,Elizabeth Anne Mellinger,1,2,жен,41,0,1,19.5,Саутгемптон +274,Charles H Natsch,0,1,муж,37,0,1,29.7,Шербур +275,Hanora Nora Healy,1,3,жен,,0,0,7.75,Квинстаун +276,Kornelia Theodosia Andrews,1,1,жен,63,1,0,77.9583,Саутгемптон +277,Augusta Charlotta Lindblom,0,3,жен,45,0,0,7.75,Саутгемптон +278,Francis Frank Parkes,0,2,муж,,0,0,0,Саутгемптон +279,Eric Rice,0,3,муж,7,4,1,29.125,Квинстаун +280,Stanton Rosa Abbott,1,3,жен,35,1,1,20.25,Саутгемптон +281,Frank Duane,0,3,муж,65,0,0,7.75,Квинстаун +282,Nils Johan Olsson,0,3,муж,28,0,0,7.8542,Саутгемптон +283,Alfons de Pelsmaeker,0,3,муж,16,0,0,9.5,Саутгемптон +284,Edward Arthur Dorking,1,3,муж,19,0,0,8.05,Саутгемптон +285,Richard William Smith,0,1,муж,,0,0,26,Саутгемптон +286,Ivan Stankovic,0,3,муж,33,0,0,8.6625,Шербур +287,Theodore de Mulder,1,3,муж,30,0,0,9.5,Саутгемптон +288,Penko Naidenoff,0,3,муж,22,0,0,7.8958,Саутгемптон +289,Masabumi Hosono,1,2,муж,42,0,0,13,Саутгемптон +290,Kate Connolly,1,3,жен,22,0,0,7.75,Квинстаун +291,Ellen Nellie Barber,1,1,жен,26,0,0,78.85,Саутгемптон +292,Dickinson H Bishop,1,1,жен,19,1,0,91.0792,Шербур +293,Rene Jacques Levy,0,2,муж,36,0,0,12.875,Шербур +294,Aloisia Haas,0,3,жен,24,0,0,8.85,Саутгемптон +295,Ivan Mineff,0,3,муж,24,0,0,7.8958,Саутгемптон +296,Ervin G Lewy,0,1,муж,,0,0,27.7208,Шербур +297,Mansour Hanna,0,3,муж,23.5,0,0,7.2292,Шербур +298,Helen Loraine Allison,0,1,жен,2,1,2,151.55,Саутгемптон +299,Adolphe Saalfeld,1,1,муж,,0,0,30.5,Саутгемптон +300,James Helene Baxter,1,1,жен,50,0,1,247.5208,Шербур +301,Anna Katherine Kelly,1,3,жен,,0,0,7.75,Квинстаун +302,Bernard McCoy,1,3,муж,,2,0,23.25,Квинстаун +303,William Cahoone Johnson,0,3,муж,19,0,0,0,Саутгемптон +304,Nora A Keane,1,2,жен,,0,0,12.35,Квинстаун +305,Howard Hugh Williams,0,3,муж,,0,0,8.05,Саутгемптон +306,Hudson Trevor Allison,1,1,муж,0.92,1,2,151.55,Саутгемптон +307,Margaret Fleming,1,1,жен,,0,0,110.8833,Шербур +308,Victor de Penasco,1,1,жен,17,1,0,108.9,Шербур +309,Samuel Abelson,0,2,муж,30,1,0,24,Шербур +310,Laura Mabel Francatelli,1,1,жен,30,0,0,56.9292,Шербур +311,Margaret Bechstein Hays,1,1,жен,24,0,0,83.1583,Шербур +312,Emily Borie Ryerson,1,1,жен,18,2,2,262.375,Шербур +313,William Anna Lahtinen,0,2,жен,26,1,1,26,Саутгемптон +314,Ignjac Hendekovic,0,3,муж,28,0,0,7.8958,Саутгемптон +315,Benjamin Hart,0,2,муж,43,1,1,26.25,Саутгемптон +316,Helmina Josefina Nilsson,1,3,жен,26,0,0,7.8542,Саутгемптон +317,Sinai Miriam Kantor,1,2,жен,24,1,0,26,Саутгемптон +318,Ernest Moraweck,0,2,муж,54,0,0,14,Саутгемптон +319,Mary Natalie Wick,1,1,жен,31,0,2,164.8667,Саутгемптон +320,Frederic Oakley Spedden,1,1,жен,40,1,1,134.5,Шербур +321,Samuel Dennis,0,3,муж,22,0,0,7.25,Саутгемптон +322,Yoto Danoff,0,3,муж,27,0,0,7.8958,Саутгемптон +323,Hilda Mary Slayter,1,2,жен,30,0,0,12.35,Квинстаун +324,Albert Francis Caldwell,1,2,жен,22,1,1,29,Саутгемптон +325,George John Sage,0,3,муж,,8,2,69.55,Саутгемптон +326,Marie Grice Young,1,1,жен,36,0,0,135.6333,Шербур +327,Johan Hansen Nysveen,0,3,муж,61,0,0,6.2375,Саутгемптон +328,Ada E Ball,1,2,жен,36,0,0,13,Саутгемптон +329,Frank John Goldsmith,1,3,жен,31,1,1,20.525,Саутгемптон +330,Jean Gertrude Hippach,1,1,жен,16,0,1,57.9792,Шербур +331,Agnes McCoy,1,3,жен,,2,0,23.25,Квинстаун +332,Austen Partner,0,1,муж,45.5,0,0,28.5,Саутгемптон +333,George Edward Graham,0,1,муж,38,0,1,153.4625,Саутгемптон +334,Leo Edmondus Vander Planke,0,3,муж,16,2,0,18,Саутгемптон +335,Henry William Frauenthal,1,1,жен,,1,0,133.65,Саутгемптон +336,Mitto Denkoff,0,3,муж,,0,0,7.8958,Саутгемптон +337,Thomas Clinton Pears,0,1,муж,29,1,0,66.6,Саутгемптон +338,Elizabeth Margaret Burns,1,1,жен,41,0,0,134.5,Шербур +339,Karl Edwart Dahl,1,3,муж,45,0,0,8.05,Саутгемптон +340,Stephen Weart Blackwell,0,1,муж,45,0,0,35.5,Саутгемптон +341,Edmond Roger Navratil,1,2,муж,2,1,1,26,Саутгемптон +342,Alice Elizabeth Fortune,1,1,жен,24,3,2,263,Саутгемптон +343,Erik Gustaf Collander,0,2,муж,28,0,0,13,Саутгемптон +344,Charles Frederick Sedgwick,0,2,муж,25,0,0,13,Саутгемптон +345,Stanley Hubert Fox,0,2,муж,36,0,0,13,Саутгемптон +346,Amelia Mildred Brown,1,2,жен,24,0,0,13,Саутгемптон +347,Marion Elsie Smith,1,2,жен,40,0,0,13,Саутгемптон +348,Thomas Henry Davison,1,3,жен,,1,0,16.1,Саутгемптон +349,William Loch Coutts,1,3,муж,3,1,1,15.9,Саутгемптон +350,Jovan Dimic,0,3,муж,42,0,0,8.6625,Саутгемптон +351,Nils Martin Odahl,0,3,муж,23,0,0,9.225,Саутгемптон +352,Fletcher Williams-Lambert,0,1,муж,,0,0,35,Саутгемптон +353,Tannous Elias,0,3,муж,15,1,1,7.2292,Шербур +354,Josef Arnold-Franchi,0,3,муж,25,1,0,17.8,Саутгемптон +355,Wazli Yousif,0,3,муж,,0,0,7.225,Шербур +356,Leo Peter Vanden Steen,0,3,муж,28,0,0,9.5,Саутгемптон +357,Elsie Edith Bowerman,1,1,жен,22,0,1,55,Саутгемптон +358,Annie Clemmer Funk,0,2,жен,38,0,0,13,Саутгемптон +359,Mary McGovern,1,3,жен,,0,0,7.8792,Квинстаун +360,Helen Mary Mockler,1,3,жен,,0,0,7.8792,Квинстаун +361,Wilhelm Skoog,0,3,муж,40,1,4,27.9,Саутгемптон +362,Sebastiano del Carlo,0,2,муж,29,1,0,27.7208,Шербур +363,Catherine David Barbara,0,3,жен,45,0,1,14.4542,Шербур +364,Adola Asim,0,3,муж,35,0,0,7.05,Саутгемптон +365,Thomas O'Brien,0,3,муж,,1,0,15.5,Квинстаун +366,Mauritz Nils Adahl,0,3,муж,30,0,0,7.25,Саутгемптон +367,Frank Manley Warren,1,1,жен,60,1,0,75.25,Шербур +368,Mantoura Boulos Moussa,1,3,жен,,0,0,7.2292,Шербур +369,Annie Jermyn,1,3,жен,,0,0,7.75,Квинстаун +370,Leontine Pauline Aubart,1,1,жен,24,0,0,69.3,Шербур +371,George Achilles Harder,1,1,муж,25,1,0,55.4417,Шербур +372,Jakob Alfred Wiklund,0,3,муж,18,1,0,6.4958,Саутгемптон +373,William Thomas Beavan,0,3,муж,19,0,0,8.05,Саутгемптон +374,Sante Ringhini,0,1,муж,22,0,0,135.6333,Шербур +375,Stina Viola Palsson,0,3,жен,3,3,1,21.075,Саутгемптон +376,Edgar Joseph Meyer,1,1,жен,,1,0,82.1708,Шербур +377,Aurora Adelia Landergren,1,3,жен,22,0,0,7.25,Саутгемптон +378,Harry Elkins Widener,0,1,муж,27,0,2,211.5,Шербур +379,Tannous Betros,0,3,муж,20,0,0,4.0125,Шербур +380,Karl Gideon Gustafsson,0,3,муж,19,0,0,7.775,Саутгемптон +381,Rosalie Bidois,1,1,жен,42,0,0,227.525,Шербур +382,Maria Mary Nakid,1,3,жен,1,0,2,15.7417,Шербур +383,Juho Tikkanen,0,3,муж,32,0,0,7.925,Саутгемптон +384,Alexander Oskar Holverson,1,1,жен,35,1,0,52,Саутгемптон +385,Vasil Plotcharsky,0,3,муж,,0,0,7.8958,Саутгемптон +386,Charles Henry Davies,0,2,муж,18,0,0,73.5,Саутгемптон +387,Sidney Leonard Goodwin,0,3,муж,1,5,2,46.9,Саутгемптон +388,Kate Buss,1,2,жен,36,0,0,13,Саутгемптон +389,Matthew Sadlier,0,3,муж,,0,0,7.7292,Квинстаун +390,Bertha Lehmann,1,2,жен,17,0,0,12,Шербур +391,William Ernest Carter,1,1,муж,36,1,2,120,Саутгемптон +392,Carl Olof Jansson,1,3,муж,21,0,0,7.7958,Саутгемптон +393,Johan Birger Gustafsson,0,3,муж,28,2,0,7.925,Саутгемптон +394,Marjorie Newell,1,1,жен,23,1,0,113.275,Шербур +395,Hjalmar Agnes Sandstrom,1,3,жен,24,0,2,16.7,Саутгемптон +396,Erik Johansson,0,3,муж,22,0,0,7.7958,Саутгемптон +397,Elina Olsson,0,3,жен,31,0,0,7.8542,Саутгемптон +398,Peter David McKane,0,2,муж,46,0,0,26,Саутгемптон +399,Alfred Pain,0,2,муж,23,0,0,10.5,Саутгемптон +400,William H Trout,1,2,жен,28,0,0,12.65,Саутгемптон +401,Juha Niskanen,1,3,муж,39,0,0,7.925,Саутгемптон +402,John Adams,0,3,муж,26,0,0,8.05,Саутгемптон +403,Mari Aina Jussila,0,3,жен,21,1,0,9.825,Саутгемптон +404,Pekka Pietari Hakkarainen,0,3,муж,28,1,0,15.85,Саутгемптон +405,Marija Oreskovic,0,3,жен,20,0,0,8.6625,Саутгемптон +406,Shadrach Gale,0,2,муж,34,1,0,21,Саутгемптон +407,Charles Peter Widegren,0,3,муж,51,0,0,7.75,Саутгемптон +408,William Rowe Richards,1,2,муж,3,1,1,18.75,Саутгемптон +409,Hans Martin Birkeland,0,3,муж,21,0,0,7.775,Саутгемптон +410,Ida Lefebre,0,3,жен,,3,1,25.4667,Саутгемптон +411,Todor Sdycoff,0,3,муж,,0,0,7.8958,Саутгемптон +412,Henry Hart,0,3,муж,,0,0,6.8583,Квинстаун +413,Daisy E Minahan,1,1,жен,33,1,0,90,Квинстаун +414,Alfred Fleming Cunningham,0,2,муж,,0,0,0,Саутгемптон +415,Johan Julian Sundman,1,3,муж,44,0,0,7.925,Саутгемптон +416,Thomas Annie Meek,0,3,жен,,0,0,8.05,Саутгемптон +417,James Vivian Drew,1,2,жен,34,1,1,32.5,Саутгемптон +418,Lyyli Karoliina Silven,1,2,жен,18,0,2,13,Саутгемптон +419,William John Matthews,0,2,муж,30,0,0,13,Саутгемптон +420,Catharina Van Impe,0,3,жен,10,0,2,24.15,Саутгемптон +421,Stanio Gheorgheff,0,3,муж,,0,0,7.8958,Шербур +422,David Charters,0,3,муж,21,0,0,7.7333,Квинстаун +423,Leo Zimmerman,0,3,муж,29,0,0,7.875,Саутгемптон +424,Ernst Gilbert Danbom,0,3,жен,28,1,1,14.4,Саутгемптон +425,Viktor Richard Rosblom,0,3,муж,18,1,1,20.2125,Саутгемптон +426,Phillippe Wiseman,0,3,муж,,0,0,7.25,Саутгемптон +427,Charles V Clarke,1,2,жен,28,1,0,26,Саутгемптон +428,Kate Florence Phillips,1,2,жен,19,0,0,26,Саутгемптон +429,James Flynn,0,3,муж,,0,0,7.75,Квинстаун +430,Berk Berk Pickard,1,3,муж,32,0,0,8.05,Саутгемптон +431,Mauritz Hakan Bjornstrom,1,1,муж,28,0,0,26.55,Саутгемптон +432,Florence Thorneycroft,1,3,жен,,1,0,16.1,Саутгемптон +433,Charles Alexander Louch,1,2,жен,42,1,0,26,Саутгемптон +434,Nikolai Erland Kallio,0,3,муж,17,0,0,7.125,Саутгемптон +435,William Baird Silvey,0,1,муж,50,1,0,55.9,Саутгемптон +436,Lucile Polk Carter,1,1,жен,14,1,2,120,Саутгемптон +437,Doolina Margaret Ford,0,3,жен,21,2,2,34.375,Саутгемптон +438,Sidney Emily Richards,1,2,жен,24,2,3,18.75,Саутгемптон +439,Mark Fortune,0,1,муж,64,1,4,263,Саутгемптон +440,Johan Henrik Kvillner,0,2,муж,31,0,0,10.5,Саутгемптон +441,Benjamin Esther Hart,1,2,жен,45,1,1,26.25,Саутгемптон +442,Leon Hampe,0,3,муж,20,0,0,9.5,Саутгемптон +443,Johan Emil Petterson,0,3,муж,25,1,0,7.775,Саутгемптон +444,Encarnacion Reynaldo,1,2,жен,28,0,0,13,Саутгемптон +445,Bernt Bratthammer,1,3,муж,,0,0,8.1125,Саутгемптон +446,Washington Dodge,1,1,муж,4,0,2,81.8583,Саутгемптон +447,Madeleine Violet Mellinger,1,2,жен,13,0,1,19.5,Саутгемптон +448,Frederic Kimber Seward,1,1,муж,34,0,0,26.55,Саутгемптон +449,Marie Catherine Baclini,1,3,жен,5,2,1,19.2583,Шербур +450,Arthur Godfrey Peuchen,1,1,муж,52,0,0,30.5,Саутгемптон +451,Edwy Arthur West,0,2,муж,36,1,2,27.75,Саутгемптон +452,Ingvald Olai Hagland,0,3,муж,,1,0,19.9667,Саутгемптон +453,Benjamin Laventall Foreman,0,1,муж,30,0,0,27.75,Шербур +454,Samuel L Goldenberg,1,1,муж,49,1,0,89.1042,Шербур +455,Joseph Peduzzi,0,3,муж,,0,0,8.05,Саутгемптон +456,Ivan Jalsevac,1,3,муж,29,0,0,7.8958,Шербур +457,Francis Davis Millet,0,1,муж,65,0,0,26.55,Саутгемптон +458,Frederick R Kenyon,1,1,жен,,1,0,51.8625,Саутгемптон +459,Ellen Toomey,1,2,жен,50,0,0,10.5,Саутгемптон +460,Maurice O'Connor,0,3,муж,,0,0,7.75,Квинстаун +461,Harry Anderson,1,1,муж,48,0,0,26.55,Саутгемптон +462,William Morley,0,3,муж,34,0,0,8.05,Саутгемптон +463,Arthur H Gee,0,1,муж,47,0,0,38.5,Саутгемптон +464,Jacob Christian Milling,0,2,муж,48,0,0,13,Саутгемптон +465,Simon Maisner,0,3,муж,,0,0,8.05,Саутгемптон +466,Manuel Estanslas Goncalves,0,3,муж,38,0,0,7.05,Саутгемптон +467,William Campbell,0,2,муж,,0,0,0,Саутгемптон +468,John Montgomery Smart,0,1,муж,56,0,0,26.55,Саутгемптон +469,James Scanlan,0,3,муж,,0,0,7.725,Квинстаун +470,Helene Barbara Baclini,1,3,жен,0.75,2,1,19.2583,Шербур +471,Arthur Keefe,0,3,муж,,0,0,7.25,Саутгемптон +472,Luka Cacic,0,3,муж,38,0,0,8.6625,Саутгемптон +473,Edwy Arthur West,1,2,жен,33,1,2,27.75,Саутгемптон +474,Amin S Jerwan,1,2,жен,23,0,0,13.7917,Шербур +475,Ida Sofia Strandberg,0,3,жен,22,0,0,9.8375,Саутгемптон +476,George Quincy Clifford,0,1,муж,,0,0,52,Саутгемптон +477,Peter Henry Renouf,0,2,муж,34,1,0,21,Саутгемптон +478,Lewis Richard Braund,0,3,муж,29,1,0,7.0458,Саутгемптон +479,Nils August Karlsson,0,3,муж,22,0,0,7.5208,Саутгемптон +480,Hildur E Hirvonen,1,3,жен,2,0,1,12.2875,Саутгемптон +481,Harold Victor Goodwin,0,3,муж,9,5,2,46.9,Саутгемптон +482,Anthony Wood Frost,0,2,муж,,0,0,0,Саутгемптон +483,Richard Henry Rouse,0,3,муж,50,0,0,8.05,Саутгемптон +484,Hedwig Turkula,1,3,жен,63,0,0,9.5875,Саутгемптон +485,Dickinson H Bishop,1,1,муж,25,1,0,91.0792,Шербур +486,Jeannie Lefebre,0,3,жен,,3,1,25.4667,Саутгемптон +487,Frederick Maxfield Hoyt,1,1,жен,35,1,0,90,Саутгемптон +488,Edward Austin Kent,0,1,муж,58,0,0,29.7,Шербур +489,Francis William Somerton,0,3,муж,30,0,0,8.05,Саутгемптон +490,Eden Leslie Coutts,1,3,муж,9,1,1,15.9,Саутгемптон +491,Konrad Mathias Hagland,0,3,муж,,1,0,19.9667,Саутгемптон +492,Einar Windelov,0,3,муж,21,0,0,7.25,Саутгемптон +493,Harry Markland Molson,0,1,муж,55,0,0,30.5,Саутгемптон +494,Ramon Artagaveytia,0,1,муж,71,0,0,49.5042,Шербур +495,Edward Roland Stanley,0,3,муж,21,0,0,8.05,Саутгемптон +496,Gerious Yousseff,0,3,муж,,0,0,14.4583,Шербур +497,Elizabeth Mussey Eustis,1,1,жен,54,1,0,78.2667,Шербур +498,Frederick William Shellard,0,3,муж,,0,0,15.1,Саутгемптон +499,Hudson J Allison,0,1,жен,25,1,2,151.55,Саутгемптон +500,Olof Svensson,0,3,муж,24,0,0,7.7958,Саутгемптон +501,Petar Calic,0,3,муж,17,0,0,8.6625,Саутгемптон +502,Mary Canavan,0,3,жен,21,0,0,7.75,Квинстаун +503,Bridget Mary O'Sullivan,0,3,жен,,0,0,7.6292,Квинстаун +504,Kristina Sofia Laitinen,0,3,жен,37,0,0,9.5875,Саутгемптон +505,Roberta Maioni,1,1,жен,16,0,0,86.5,Саутгемптон +506,Victor Penasco,0,1,муж,18,1,0,108.9,Шербур +507,Frederick Charles Quick,1,2,жен,33,0,2,26,Саутгемптон +508,George George Bradley,1,1,муж,,0,0,26.55,Саутгемптон +509,Henry Margido Olsen,0,3,муж,28,0,0,22.525,Саутгемптон +510,Fang Lang,1,3,муж,26,0,0,56.4958,Саутгемптон +511,Eugene Patrick Daly,1,3,муж,29,0,0,7.75,Квинстаун +512,James Webber,0,3,муж,,0,0,8.05,Саутгемптон +513,James Robert McGough,1,1,муж,36,0,0,26.2875,Саутгемптон +514,Martin Rothschild,1,1,жен,54,1,0,59.4,Шербур +515,Satio Coleff,0,3,муж,24,0,0,7.4958,Саутгемптон +516,William Anderson Walker,0,1,муж,47,0,0,34.0208,Саутгемптон +517,Amelia Milley Lemore,1,2,жен,34,0,0,10.5,Саутгемптон +518,Patrick Ryan,0,3,муж,,0,0,24.15,Квинстаун +519,William A Angle,1,2,жен,36,1,0,26,Саутгемптон +520,Stefo Pavlovic,0,3,муж,32,0,0,7.8958,Саутгемптон +521,Anne Perreault,1,1,жен,30,0,0,93.5,Саутгемптон +522,Janko Vovk,0,3,муж,22,0,0,7.8958,Саутгемптон +523,Sarkis Lahoud,0,3,муж,,0,0,7.225,Шербур +524,Louis Albert Hippach,1,1,жен,44,0,1,57.9792,Шербур +525,Fared Kassem,0,3,муж,,0,0,7.2292,Шербур +526,James Farrell,0,3,муж,40.5,0,0,7.75,Квинстаун +527,Lucy Ridsdale,1,2,жен,50,0,0,10.5,Саутгемптон +528,John Farthing,0,1,муж,,0,0,221.7792,Саутгемптон +529,Johan Werner Salonen,0,3,муж,39,0,0,7.925,Саутгемптон +530,Richard George Hocking,0,2,муж,23,2,1,11.5,Саутгемптон +531,Phyllis May Quick,1,2,жен,2,1,1,26,Саутгемптон +532,Nakli Toufik,0,3,муж,,0,0,7.2292,Шербур +533,Joseph Jr Elias,0,3,муж,17,1,1,7.2292,Шербур +534,Catherine Catherine Peter,1,3,жен,,0,2,22.3583,Шербур +535,Marija Cacic,0,3,жен,30,0,0,8.6625,Саутгемптон +536,Eva Miriam Hart,1,2,жен,7,0,2,26.25,Саутгемптон +537,Archibald Willingham Butt,0,1,муж,45,0,0,26.55,Саутгемптон +538,Bertha LeRoy,1,1,жен,30,0,0,106.425,Шербур +539,Samuel Beard Risien,0,3,муж,,0,0,14.5,Саутгемптон +540,Hedwig Frolicher,1,1,жен,22,0,2,49.5,Шербур +541,Harriet R Crosby,1,1,жен,36,0,2,71,Саутгемптон +542,Ingeborg Andersson,0,3,жен,9,4,2,31.275,Саутгемптон +543,Sigrid Andersson,0,3,жен,11,4,2,31.275,Саутгемптон +544,Edward Beane,1,2,муж,32,1,0,26,Саутгемптон +545,Walter Donald Douglas,0,1,муж,50,1,0,106.425,Шербур +546,Arthur Ernest Nicholson,0,1,муж,64,0,0,26,Саутгемптон +547,Edward Ethel Beane,1,2,жен,19,1,0,26,Саутгемптон +548,Julian Padro y Manent,1,2,муж,,0,0,13.8625,Шербур +549,Frank John Goldsmith,0,3,муж,33,1,1,20.525,Саутгемптон +550,John Morgan Davies,1,2,муж,8,1,1,36.75,Саутгемптон +551,John Borland Thayer,1,1,муж,17,0,2,110.8833,Шербур +552,Percival James Sharp,0,2,муж,27,0,0,26,Саутгемптон +553,Timothy O'Brien,0,3,муж,,0,0,7.8292,Квинстаун +554,Fahim Philip Leeni,1,3,муж,22,0,0,7.225,Шербур +555,Velin Ohman,1,3,жен,22,0,0,7.775,Саутгемптон +556,George Wright,0,1,муж,62,0,0,26.55,Саутгемптон +557,Lucille Duff Gordon,1,1,жен,48,1,0,39.6,Шербур +558,Victor Robbins,0,1,муж,,0,0,227.525,Шербур +559,Emil Tillie Taussig,1,1,жен,39,1,1,79.65,Саутгемптон +560,Guillaume Messemaeker,1,3,жен,36,1,0,17.4,Саутгемптон +561,Thomas Rowan Morrow,0,3,муж,,0,0,7.75,Квинстаун +562,Husein Sivic,0,3,муж,40,0,0,7.8958,Саутгемптон +563,Robert Douglas Norman,0,2,муж,28,0,0,13.5,Саутгемптон +564,John Simmons,0,3,муж,,0,0,8.05,Саутгемптон +565,Marion Ogden Meanwell,0,3,жен,,0,0,8.05,Саутгемптон +566,Alfred J Davies,0,3,муж,24,2,0,24.15,Саутгемптон +567,Ilia Stoytcheff,0,3,муж,19,0,0,7.8958,Саутгемптон +568,Nils Alma Palsson,0,3,жен,29,0,4,21.075,Саутгемптон +569,Tannous Doharr,0,3,муж,,0,0,7.2292,Шербур +570,Carl Jonsson,1,3,муж,32,0,0,7.8542,Саутгемптон +571,George Harris,1,2,муж,62,0,0,10.5,Саутгемптон +572,Edward Dale Appleton,1,1,жен,53,2,0,51.4792,Саутгемптон +573,John Irwin Flynn,1,1,муж,36,0,0,26.3875,Саутгемптон +574,Mary Kelly,1,3,жен,,0,0,7.75,Квинстаун +575,Alfred George Rush,0,3,муж,16,0,0,8.05,Саутгемптон +576,George Patchett,0,3,муж,19,0,0,14.5,Саутгемптон +577,Ethel Garside,1,2,жен,34,0,0,13,Саутгемптон +578,William Baird Silvey,1,1,жен,39,1,0,55.9,Саутгемптон +579,Joseph Maria Caram,0,3,жен,,1,0,14.4583,Шербур +580,Eiriik Jussila,1,3,муж,32,0,0,7.925,Саутгемптон +581,Julie Rachel Christy,1,2,жен,25,1,1,30,Саутгемптон +582,John Borland Thayer,1,1,жен,39,1,1,110.8833,Шербур +583,William James Downton,0,2,муж,54,0,0,26,Саутгемптон +584,John Hugo Ross,0,1,муж,36,0,0,40.125,Шербур +585,Uscher Paulner,0,3,муж,,0,0,8.7125,Шербур +586,Ruth Taussig,1,1,жен,18,0,2,79.65,Саутгемптон +587,John Denzil Jarvis,0,2,муж,47,0,0,15,Саутгемптон +588,Maxmillian Frolicher-Stehli,1,1,муж,60,1,1,79.2,Шербур +589,Eliezer Gilinski,0,3,муж,22,0,0,8.05,Саутгемптон +590,Joseph Murdlin,0,3,муж,,0,0,8.05,Саутгемптон +591,Matti Rintamaki,0,3,муж,35,0,0,7.125,Саутгемптон +592,Walter Bertram Stephenson,1,1,жен,52,1,0,78.2667,Шербур +593,William James Elsbury,0,3,муж,47,0,0,7.25,Саутгемптон +594,Mary Bourke,0,3,жен,,0,2,7.75,Квинстаун +595,John Henry Chapman,0,2,муж,37,1,0,26,Саутгемптон +596,Jean Baptiste Van Impe,0,3,муж,36,1,1,24.15,Саутгемптон +597,Jessie Wills Leitch,1,2,жен,,0,0,33,Саутгемптон +598,Alfred Johnson,0,3,муж,49,0,0,0,Саутгемптон +599,Hanna Boulos,0,3,муж,,0,0,7.225,Шербур +600,Cosmo Edmund Duff Gordon,1,1,муж,49,1,0,56.9292,Шербур +601,Sidney Samuel Jacobsohn,1,2,жен,24,2,1,27,Саутгемптон +602,Petco Slabenoff,0,3,муж,,0,0,7.8958,Саутгемптон +603,Charles H Harrington,0,1,муж,,0,0,42.4,Саутгемптон +604,Ernst William Torber,0,3,муж,44,0,0,8.05,Саутгемптон +605,Harry Mr Homer,1,1,муж,35,0,0,26.55,Шербур +606,Edvard Bengtsson Lindell,0,3,муж,36,1,0,15.55,Саутгемптон +607,Milan Karaic,0,3,муж,30,0,0,7.8958,Саутгемптон +608,Robert Williams Daniel,1,1,муж,27,0,0,30.5,Саутгемптон +609,Joseph Juliette Laroche,1,2,жен,22,1,2,41.5792,Шербур +610,Elizabeth W Shutes,1,1,жен,40,0,0,153.4625,Саутгемптон +611,Anders Johan Andersson,0,3,жен,39,1,5,31.275,Саутгемптон +612,Jose Neto Jardin,0,3,муж,,0,0,7.05,Саутгемптон +613,Margaret Jane Murphy,1,3,жен,,1,0,15.5,Квинстаун +614,John Horgan,0,3,муж,,0,0,7.75,Квинстаун +615,William Alfred Brocklebank,0,3,муж,35,0,0,8.05,Саутгемптон +616,Alice Herman,1,2,жен,24,1,2,65,Саутгемптон +617,Ernst Gilbert Danbom,0,3,муж,34,1,1,14.4,Саутгемптон +618,William Arthur Lobb,0,3,жен,26,1,0,16.1,Саутгемптон +619,Marion Louise Becker,1,2,жен,4,2,1,39,Саутгемптон +620,Lawrence Gavey,0,2,муж,26,0,0,10.5,Саутгемптон +621,Antoni Yasbeck,0,3,муж,27,1,0,14.4542,Шербур +622,Edwin Nelson Kimball,1,1,муж,42,1,0,52.5542,Саутгемптон +623,Sahid Nakid,1,3,муж,20,1,1,15.7417,Шербур +624,Henry Damsgaard Hansen,0,3,муж,21,0,0,7.8542,Саутгемптон +625,David John Bowen,0,3,муж,21,0,0,16.1,Саутгемптон +626,Frederick Sutton,0,1,муж,61,0,0,32.3208,Саутгемптон +627,Charles Leonard Kirkland,0,2,муж,57,0,0,12.35,Квинстаун +628,Gretchen Fiske Longley,1,1,жен,21,0,0,77.9583,Саутгемптон +629,Guentcho Bostandyeff,0,3,муж,26,0,0,7.8958,Саутгемптон +630,Patrick D O'Connell,0,3,муж,,0,0,7.7333,Квинстаун +631,Algernon Henry Barkworth,1,1,муж,80,0,0,30,Саутгемптон +632,Johan Svensson Lundahl,0,3,муж,51,0,0,7.0542,Саутгемптон +633,Max Stahelin-Maeglin,1,1,муж,32,0,0,30.5,Шербур +634,William Henry Parr,0,1,муж,,0,0,0,Саутгемптон +635,Mabel Skoog,0,3,жен,9,3,2,27.9,Саутгемптон +636,Mary Davis,1,2,жен,28,0,0,13,Саутгемптон +637,Antti Gustaf Leinonen,0,3,муж,32,0,0,7.925,Саутгемптон +638,Harvey Collyer,0,2,муж,31,1,1,26.25,Саутгемптон +639,Juha Maria Panula,0,3,жен,41,0,5,39.6875,Саутгемптон +640,Percival Thorneycroft,0,3,муж,,1,0,16.1,Саутгемптон +641,Hans Peder Jensen,0,3,муж,20,0,0,7.8542,Саутгемптон +642,Emma Sagesser,1,1,жен,24,0,0,69.3,Шербур +643,Margit Elizabeth Skoog,0,3,жен,2,3,2,27.9,Саутгемптон +644,Choong Foo,1,3,муж,,0,0,56.4958,Саутгемптон +645,Eugenie Baclini,1,3,жен,0.75,2,1,19.2583,Шербур +646,Henry Sleeper Harper,1,1,муж,48,1,0,76.7292,Шербур +647,Liudevit Cor,0,3,муж,19,0,0,7.8958,Саутгемптон +648,Alfons Simonius-Blumer,1,1,муж,56,0,0,35.5,Шербур +649,Edward Willey,0,3,муж,,0,0,7.55,Саутгемптон +650,Amy Zillah Stanley,1,3,жен,23,0,0,7.55,Саутгемптон +651,Mito Mitkoff,0,3,муж,,0,0,7.8958,Саутгемптон +652,Elsie Doling,1,2,жен,18,0,1,23,Саутгемптон +653,Johannes Halvorsen Kalvik,0,3,муж,21,0,0,8.4333,Саутгемптон +654,Hanora Norah O'Leary,1,3,жен,,0,0,7.8292,Квинстаун +655,Hanora Nora Hegarty,0,3,жен,18,0,0,6.75,Квинстаун +656,Leonard Mark Hickman,0,2,муж,24,2,0,73.5,Саутгемптон +657,Alexander Radeff,0,3,муж,,0,0,7.8958,Саутгемптон +658,John Catherine Bourke,0,3,жен,32,1,1,15.5,Квинстаун +659,George Floyd Eitemiller,0,2,муж,23,0,0,13,Саутгемптон +660,Arthur Webster Newell,0,1,муж,58,0,2,113.275,Шербур +661,Henry William Frauenthal,1,1,муж,50,2,0,133.65,Саутгемптон +662,Mohamed Badt,0,3,муж,40,0,0,7.225,Шербур +663,Edward Pomeroy Colley,0,1,муж,47,0,0,25.5875,Саутгемптон +664,Peju Coleff,0,3,муж,36,0,0,7.4958,Саутгемптон +665,Eino William Lindqvist,1,3,муж,20,1,0,7.925,Саутгемптон +666,Lewis Hickman,0,2,муж,32,2,0,73.5,Саутгемптон +667,Reginald Fenton Butler,0,2,муж,25,0,0,13,Саутгемптон +668,Knud Paust Rommetvedt,0,3,муж,,0,0,7.775,Саутгемптон +669,Jacob Cook,0,3,муж,43,0,0,8.05,Саутгемптон +670,Elmer Zebley Taylor,1,1,жен,,1,0,52,Саутгемптон +671,Thomas William Brown,1,2,жен,40,1,1,39,Саутгемптон +672,Thornton Davidson,0,1,муж,31,1,0,52,Саутгемптон +673,Henry Michael Mitchell,0,2,муж,70,0,0,10.5,Саутгемптон +674,Charles Wilhelms,1,2,муж,31,0,0,13,Саутгемптон +675,Ennis Hastings Watson,0,2,муж,,0,0,0,Саутгемптон +676,Gustaf Hjalmar Edvardsson,0,3,муж,18,0,0,7.775,Саутгемптон +677,Frederick Charles Sawyer,0,3,муж,24.5,0,0,8.05,Саутгемптон +678,Anna Sofia Turja,1,3,жен,18,0,0,9.8417,Саутгемптон +679,Frederick Augusta Goodwin,0,3,жен,43,1,6,46.9,Саутгемптон +680,Thomas Drake Cardeza,1,1,муж,36,0,1,512.3292,Шербур +681,Katie Peters,0,3,жен,,0,0,8.1375,Квинстаун +682,Hammad Hassab,1,1,муж,27,0,0,76.7292,Шербур +683,Thor Anderson Olsvigen,0,3,муж,20,0,0,9.225,Саутгемптон +684,Charles Edward Goodwin,0,3,муж,14,5,2,46.9,Саутгемптон +685,Thomas William Brown,0,2,муж,60,1,1,39,Саутгемптон +686,Joseph Philippe Laroche,0,2,муж,25,1,2,41.5792,Шербур +687,Jaako Arnold Panula,0,3,муж,14,4,1,39.6875,Саутгемптон +688,Branko Dakic,0,3,муж,19,0,0,10.1708,Саутгемптон +689,Eberhard Thelander Fischer,0,3,муж,18,0,0,7.7958,Саутгемптон +690,Georgette Alexandra Madill,1,1,жен,15,0,1,211.3375,Саутгемптон +691,Albert Adrian Dick,1,1,муж,31,1,0,57,Саутгемптон +692,Manca Karun,1,3,жен,4,0,1,13.4167,Шербур +693,Ali Lam,1,3,муж,,0,0,56.4958,Саутгемптон +694,Khalil Saad,0,3,муж,25,0,0,7.225,Шербур +695,John Weir,0,1,муж,60,0,0,26.55,Саутгемптон +696,Charles Henry Chapman,0,2,муж,52,0,0,13.5,Саутгемптон +697,James Kelly,0,3,муж,44,0,0,8.05,Саутгемптон +698,Katherine Katie Mullens,1,3,жен,,0,0,7.7333,Квинстаун +699,John Borland Thayer,0,1,муж,49,1,1,110.8833,Шербур +700,Adolf Mathias Humblen,0,3,муж,42,0,0,7.65,Саутгемптон +701,John Jacob Astor,1,1,жен,18,1,0,227.525,Шербур +702,Spencer Victor Silverthorne,1,1,муж,35,0,0,26.2875,Саутгемптон +703,Saiide Barbara,0,3,жен,18,0,1,14.4542,Шербур +704,Martin Gallagher,0,3,муж,25,0,0,7.7417,Квинстаун +705,Henrik Juul Hansen,0,3,муж,26,1,0,7.8542,Саутгемптон +706,Henry Samuel Morley,0,2,муж,39,0,0,26,Саутгемптон +707,Florence Fannie Kelly,1,2,жен,45,0,0,13.5,Саутгемптон +708,Edward Calderhead,1,1,муж,42,0,0,26.2875,Саутгемптон +709,Alice Cleaver,1,1,жен,22,0,0,151.55,Саутгемптон +710,Halim Gonios Moubarek,1,3,муж,,1,1,15.2458,Шербур +711,Berthe Antonine Mayne,1,1,жен,24,0,0,49.5042,Шербур +712,Herman Klaber,0,1,муж,,0,0,26.55,Саутгемптон +713,Elmer Zebley Taylor,1,1,муж,48,1,0,52,Саутгемптон +714,August Viktor Larsson,0,3,муж,29,0,0,9.4833,Саутгемптон +715,Samuel Greenberg,0,2,муж,52,0,0,13,Саутгемптон +716,Peter Andreas Soholt,0,3,муж,19,0,0,7.65,Саутгемптон +717,Caroline Louise Endres,1,1,жен,38,0,0,227.525,Шербур +718,Edwina Celia Troutt,1,2,жен,27,0,0,10.5,Саутгемптон +719,Michael McEvoy,0,3,муж,,0,0,15.5,Квинстаун +720,Malkolm Joackim Johnson,0,3,муж,33,0,0,7.775,Саутгемптон +721,Annie Jessie Harper,1,2,жен,6,0,1,33,Саутгемптон +722,Svend Lauritz Jensen,0,3,муж,17,1,0,7.0542,Саутгемптон +723,William Henry Gillespie,0,2,муж,34,0,0,13,Саутгемптон +724,Henry Price Hodges,0,2,муж,50,0,0,13,Саутгемптон +725,Norman Campbell Chambers,1,1,муж,27,1,0,53.1,Саутгемптон +726,Luka Oreskovic,0,3,муж,20,0,0,8.6625,Саутгемптон +727,Peter Henry Renouf,1,2,жен,30,3,0,21,Саутгемптон +728,Margareth Mannion,1,3,жен,,0,0,7.7375,Квинстаун +729,Kurt Arnold Bryhl,0,2,муж,25,1,0,26,Саутгемптон +730,Pieta Sofia Ilmakangas,0,3,жен,25,1,0,7.925,Саутгемптон +731,Elisabeth Walton Allen,1,1,жен,29,0,0,211.3375,Саутгемптон +732,Houssein G Hassan,0,3,муж,11,0,0,18.7875,Шербур +733,Robert J Knight,0,2,муж,,0,0,0,Саутгемптон +734,William John Berriman,0,2,муж,23,0,0,13,Саутгемптон +735,Moses Aaron Troupiansky,0,2,муж,23,0,0,13,Саутгемптон +736,Leslie Williams,0,3,муж,28.5,0,0,16.1,Саутгемптон +737,Edward Margaret Ford,0,3,жен,48,1,3,34.375,Саутгемптон +738,Gustave J Lesurer,1,1,муж,35,0,0,512.3292,Шербур +739,Kanio Ivanoff,0,3,муж,,0,0,7.8958,Саутгемптон +740,Minko Nankoff,0,3,муж,,0,0,7.8958,Саутгемптон +741,Walter James Hawksford,1,1,муж,,0,0,30,Саутгемптон +742,Tyrell William Cavendish,0,1,муж,36,1,0,78.85,Саутгемптон +743,Susan Parker Ryerson,1,1,жен,21,2,2,262.375,Шербур +744,Neal McNamee,0,3,муж,24,1,0,16.1,Саутгемптон +745,Juho Stranden,1,3,муж,31,0,0,7.925,Саутгемптон +746,Edward Gifford Crosby,0,1,муж,70,1,1,71,Саутгемптон +747,Rossmore Edward Abbott,0,3,муж,16,1,1,20.25,Саутгемптон +748,Anna Sinkkonen,1,2,жен,30,0,0,13,Саутгемптон +749,Daniel Warner Marvin,0,1,муж,19,1,0,53.1,Саутгемптон +750,Michael Connaghton,0,3,муж,31,0,0,7.75,Квинстаун +751,Joan Wells,1,2,жен,4,1,1,23,Саутгемптон +752,Meier Moor,1,3,муж,6,0,1,12.475,Саутгемптон +753,Johannes Vande Velde,0,3,муж,33,0,0,9.5,Саутгемптон +754,Lalio Jonkoff,0,3,муж,23,0,0,7.8958,Саутгемптон +755,Samuel Jane Herman,1,2,жен,48,1,2,65,Саутгемптон +756,Viljo Hamalainen,1,2,муж,0.67,1,1,14.5,Саутгемптон +757,August Sigfrid Carlsson,0,3,муж,28,0,0,7.7958,Саутгемптон +758,Percy Andrew Bailey,0,2,муж,18,0,0,11.5,Саутгемптон +759,Thomas Leonard Theobald,0,3,муж,34,0,0,8.05,Саутгемптон +760,of Lucy Rothes,1,1,жен,33,0,0,86.5,Саутгемптон +761,John Garfirth,0,3,муж,,0,0,14.5,Саутгемптон +762,Iisakki Antino Nirva,0,3,муж,41,0,0,7.125,Саутгемптон +763,Hanna Assi Barah,1,3,муж,20,0,0,7.2292,Шербур +764,William Ernest Carter,1,1,жен,36,1,2,120,Саутгемптон +765,Hans Linus Eklund,0,3,муж,16,0,0,7.775,Саутгемптон +766,John C Hogeboom,1,1,жен,51,1,0,77.9583,Саутгемптон +767,Arthur Jackson Brewe,0,1,муж,,0,0,39.6,Шербур +768,Mary Mangan,0,3,жен,30.5,0,0,7.75,Квинстаун +769,Daniel J Moran,0,3,муж,,1,0,24.15,Квинстаун +770,Daniel Gronnestad,0,3,муж,32,0,0,8.3625,Саутгемптон +771,Rene Aime Lievens,0,3,муж,24,0,0,9.5,Саутгемптон +772,Niels Peder Jensen,0,3,муж,48,0,0,7.8542,Саутгемптон +773,Mary Mack,0,2,жен,57,0,0,10.5,Саутгемптон +774,Dibo Elias,0,3,муж,,0,0,7.225,Шербур +775,Elizabeth Eliza Hocking,1,2,жен,54,1,3,23,Саутгемптон +776,Pehr Fabian Myhrman,0,3,муж,18,0,0,7.75,Саутгемптон +777,Roger Tobin,0,3,муж,,0,0,7.75,Квинстаун +778,Virginia Ethel Emanuel,1,3,жен,5,0,0,12.475,Саутгемптон +779,Thomas J Kilgannon,0,3,муж,,0,0,7.7375,Квинстаун +780,Edward Scott Robert,1,1,жен,43,0,1,211.3375,Саутгемптон +781,Banoura Ayoub,1,3,жен,13,0,0,7.2292,Шербур +782,Albert Adrian Dick,1,1,жен,17,1,0,57,Саутгемптон +783,Milton Clyde Long,0,1,муж,29,0,0,30,Саутгемптон +784,Andrew G Johnston,0,3,муж,,1,2,23.45,Саутгемптон +785,William Ali,0,3,муж,25,0,0,7.05,Саутгемптон +786,Abraham David Harmer,0,3,муж,25,0,0,7.25,Саутгемптон +787,Anna Sofia Sjoblom,1,3,жен,18,0,0,7.4958,Саутгемптон +788,George Hugh Rice,0,3,муж,8,4,1,29.125,Квинстаун +789,Bertram Vere Dean,1,3,муж,1,1,2,20.575,Саутгемптон +790,Benjamin Guggenheim,0,1,муж,46,0,0,79.2,Шербур +791,Andrew Andy Keane,0,3,муж,,0,0,7.75,Квинстаун +792,Alfred Gaskell,0,2,муж,16,0,0,26,Саутгемптон +793,Stella Anna Sage,0,3,жен,,8,2,69.55,Саутгемптон +794,William Fisher Hoyt,0,1,муж,,0,0,30.6958,Шербур +795,Ristiu Dantcheff,0,3,муж,25,0,0,7.8958,Саутгемптон +796,Richard Otter,0,2,муж,39,0,0,13,Саутгемптон +797,Alice Farnham Leader,1,1,жен,49,0,0,25.9292,Саутгемптон +798,Mara Osman,1,3,жен,31,0,0,8.6833,Саутгемптон +799,Yousseff Ibrahim Shawah,0,3,муж,30,0,0,7.2292,Шербур +800,Jean Baptiste Van Impe,0,3,жен,30,1,1,24.15,Саутгемптон +801,Martin Ponesell,0,2,муж,34,0,0,13,Саутгемптон +802,Harvey Charlotte Collyer,1,2,жен,31,1,1,26.25,Саутгемптон +803,William Thornton Carter,1,1,муж,11,1,2,120,Саутгемптон +804,Assad Alexander Thomas,1,3,муж,0.42,0,1,8.5167,Шербур +805,Oskar Arvid Hedman,1,3,муж,27,0,0,6.975,Саутгемптон +806,Karl Johan Johansson,0,3,муж,31,0,0,7.775,Саутгемптон +807,Thomas Jr Andrews,0,1,муж,39,0,0,0,Саутгемптон +808,Ellen Natalia Pettersson,0,3,жен,18,0,0,7.775,Саутгемптон +809,August Meyer,0,2,муж,39,0,0,13,Саутгемптон +810,Norman Campbell Chambers,1,1,жен,33,1,0,53.1,Саутгемптон +811,William Alexander,0,3,муж,26,0,0,7.8875,Саутгемптон +812,James Lester,0,3,муж,39,0,0,24.15,Саутгемптон +813,Richard James Slemen,0,2,муж,35,0,0,10.5,Саутгемптон +814,Ebba Iris Andersson,0,3,жен,6,4,2,31.275,Саутгемптон +815,Ernest Portage Tomlin,0,3,муж,30.5,0,0,8.05,Саутгемптон +816,Richard Fry,0,1,муж,,0,0,0,Саутгемптон +817,Wendla Maria Heininen,0,3,жен,23,0,0,7.925,Саутгемптон +818,Albert Mallet,0,2,муж,31,1,1,37.0042,Шербур +819,John Fredrik Holm,0,3,муж,43,0,0,6.45,Саутгемптон +820,Karl Thorsten Skoog,0,3,муж,10,3,2,27.9,Саутгемптон +821,Charles Melville Hays,1,1,жен,52,1,1,93.5,Саутгемптон +822,Nikola Lulic,1,3,муж,27,0,0,8.6625,Саутгемптон +823,John George Reuchlin,0,1,муж,38,0,0,0,Саутгемптон +824,Beila Moor,1,3,жен,27,0,1,12.475,Саутгемптон +825,Urho Abraham Panula,0,3,муж,2,4,1,39.6875,Саутгемптон +826,John Flynn,0,3,муж,,0,0,6.95,Квинстаун +827,Len Lam,0,3,муж,,0,0,56.4958,Саутгемптон +828,Andre Mallet,1,2,муж,1,0,2,37.0042,Шербур +829,Thomas Joseph McCormack,1,3,муж,,0,0,7.75,Квинстаун +830,George Nelson Stone,1,1,жен,62,0,0,80, +831,Antoni Selini Yasbeck,1,3,жен,15,1,0,14.4542,Шербур +832,George Sibley Richards,1,2,муж,0.83,1,1,18.75,Саутгемптон +833,Amin Saad,0,3,муж,,0,0,7.2292,Шербур +834,Albert Augustsson,0,3,муж,23,0,0,7.8542,Саутгемптон +835,Owen George Allum,0,3,муж,18,0,0,8.3,Саутгемптон +836,Sara Rebecca Compton,1,1,жен,39,1,1,83.1583,Шербур +837,Jakob Pasic,0,3,муж,21,0,0,8.6625,Саутгемптон +838,Maurice Sirota,0,3,муж,,0,0,8.05,Саутгемптон +839,Chang Chip,1,3,муж,32,0,0,56.4958,Саутгемптон +840,Pierre Marechal,1,1,муж,,0,0,29.7,Шербур +841,Ilmari Rudolf Alhomaki,0,3,муж,20,0,0,7.925,Саутгемптон +842,Thomas Charles Mudd,0,2,муж,16,0,0,10.5,Саутгемптон +843,Augusta Serepeca,1,1,жен,30,0,0,31,Шербур +844,Peter L Lemberopolous,0,3,муж,34.5,0,0,6.4375,Шербур +845,Jeso Culumovic,0,3,муж,17,0,0,8.6625,Саутгемптон +846,Anthony Abbing,0,3,муж,42,0,0,7.55,Саутгемптон +847,Douglas Bullen Sage,0,3,муж,,8,2,69.55,Саутгемптон +848,Marin Markoff,0,3,муж,35,0,0,7.8958,Шербур +849,John Harper,0,2,муж,28,0,1,33,Саутгемптон +850,Samuel L Goldenberg,1,1,жен,,1,0,89.1042,Шербур +851,Sigvard Harald Andersson,0,3,муж,4,4,2,31.275,Саутгемптон +852,Johan Svensson,0,3,муж,74,0,0,7.775,Саутгемптон +853,Nourelain Boulos,0,3,жен,9,1,1,15.2458,Шербур +854,Mary Conover Lines,1,1,жен,16,0,1,39.4,Саутгемптон +855,Ernest Courtenay Carter,0,2,жен,44,1,0,26,Саутгемптон +856,Sam Leah Aks,1,3,жен,18,0,1,9.35,Саутгемптон +857,George Dennick Wick,1,1,жен,45,1,1,164.8667,Саутгемптон +858,Peter Denis Daly,1,1,муж,51,0,0,26.55,Саутгемптон +859,Solomon Latifa Baclini,1,3,жен,24,0,3,19.2583,Шербур +860,Raihed Razi,0,3,муж,,0,0,7.2292,Шербур +861,Claus Peter Hansen,0,3,муж,41,2,0,14.1083,Саутгемптон +862,Frederick Edward Giles,0,2,муж,21,1,0,11.5,Саутгемптон +863,Frederick Joel Swift,1,1,жен,48,0,0,25.9292,Саутгемптон +864,Dorothy Edith Sage,0,3,жен,,8,2,69.55,Саутгемптон +865,John William Gill,0,2,муж,24,0,0,13,Саутгемптон +866,Karolina Bystrom,1,2,жен,42,0,0,13,Саутгемптон +867,Asuncion Duran y More,1,2,жен,27,1,0,13.8583,Шербур +868,Washington Roebling,0,1,муж,31,0,0,50.4958,Саутгемптон +869,Philemon van Melkebeke,0,3,муж,,0,0,9.5,Саутгемптон +870,Harold Theodor Johnson,1,3,муж,4,1,1,11.1333,Саутгемптон +871,Cerin Balkic,0,3,муж,26,0,0,7.8958,Саутгемптон +872,Richard Leonard Beckwith,1,1,жен,47,1,1,52.5542,Саутгемптон +873,Frans Olof Carlsson,0,1,муж,33,0,0,5,Саутгемптон +874,Victor Vander Cruyssen,0,3,муж,47,0,0,9,Саутгемптон +875,Samuel Hannah Abelson,1,2,жен,28,1,0,24,Шербур +876,Adele Kiamie Najib,1,3,жен,15,0,0,7.225,Шербур +877,Alfred Ossian Gustafsson,0,3,муж,20,0,0,9.8458,Саутгемптон +878,Nedelio Petroff,0,3,муж,19,0,0,7.8958,Саутгемптон +879,Kristo Laleff,0,3,муж,,0,0,7.8958,Саутгемптон +880,Thomas Jr Potter,1,1,жен,56,0,1,83.1583,Шербур +881,William Imanita Shelley,1,2,жен,25,0,1,26,Саутгемптон +882,Johann Markun,0,3,муж,33,0,0,7.8958,Саутгемптон +883,Gerda Ulrika Dahlberg,0,3,жен,22,0,0,10.5167,Саутгемптон +884,Frederick James Banfield,0,2,муж,28,0,0,10.5,Саутгемптон +885,Henry Jr Sutehall,0,3,муж,25,0,0,7.05,Саутгемптон +886,William Margaret Rice,0,3,жен,39,0,5,29.125,Квинстаун +887,Juozas Montvila,0,2,муж,27,0,0,13,Саутгемптон +888,Margaret Edith Graham,1,1,жен,19,0,0,30,Саутгемптон +889,Catherine Helen Johnston,0,3,жен,,1,2,23.45,Саутгемптон +890,Karl Howell Behr,1,1,муж,26,0,0,30,Шербур +891,Patrick Dooley,0,3,муж,32,0,0,7.75,Квинстаун diff --git a/ReleaseGenerators/Files/Datasets/titanic_ru.meta b/ReleaseGenerators/Files/Datasets/titanic_ru.meta new file mode 100644 index 000000000..479e9606b --- /dev/null +++ b/ReleaseGenerators/Files/Datasets/titanic_ru.meta @@ -0,0 +1,64 @@ +name = TitanicRu + +task = classification + +features = Класс,Пол,Возраст,БратьяИСупруги,РодителиИДети,ЦенаБилета,ПортПосадки +target = Выжил + +description.en = Titanic passengers dataset with simplified Russian column names for binary survival classification. +description.ru = Датасет пассажиров Титаника с упрощёнными русскими названиями полей для задачи бинарной классификации выживания. + +source = Kaggle: Titanic - Machine Learning from Disaster +url = https://www.kaggle.com/competitions/titanic/data + +feature.Id = int +feature.Имя = categorical +feature.Выжил = categorical +feature.Класс = categorical +feature.Пол = categorical +feature.Возраст = float +feature.БратьяИСупруги = int +feature.РодителиИДети = int +feature.ЦенаБилета = float +feature.ПортПосадки = categorical + +feature.Id.ru = номер пассажира +feature.Имя.ru = имя пассажира +feature.Выжил.ru = выжил +feature.Класс.ru = класс билета +feature.Пол.ru = пол +feature.Возраст.ru = возраст +feature.БратьяИСупруги.ru = количество братьев, сестёр и супругов на борту +feature.РодителиИДети.ru = количество родителей и детей на борту +feature.ЦенаБилета.ru = цена билета +feature.ПортПосадки.ru = порт посадки + +feature.Id.en = passenger id +feature.Имя.en = passenger name +feature.Выжил.en = survived +feature.Класс.en = passenger class +feature.Пол.en = sex +feature.Возраст.en = age +feature.БратьяИСупруги.en = siblings and spouses aboard +feature.РодителиИДети.en = parents and children aboard +feature.ЦенаБилета.en = ticket fare +feature.ПортПосадки.en = port of embarkation + +value.Выжил.0.ru = не выжил +value.Выжил.1.ru = выжил +value.Выжил.0.en = did not survive +value.Выжил.1.en = survived + +value.Класс.1.ru = первый класс +value.Класс.2.ru = второй класс +value.Класс.3.ru = третий класс +value.Класс.1.en = first class +value.Класс.2.en = second class +value.Класс.3.en = third class + +value.Пол.муж.en = male +value.Пол.жен.en = female + +value.ПортПосадки.Саутгемптон.en = Southampton +value.ПортПосадки.Шербур.en = Cherbourg +value.ПортПосадки.Квинстаун.en = Queenstown diff --git a/ReleaseGenerators/PascalABCNET_version.nsh b/ReleaseGenerators/PascalABCNET_version.nsh index d58b1e3ab..aec7a931e 100644 --- a/ReleaseGenerators/PascalABCNET_version.nsh +++ b/ReleaseGenerators/PascalABCNET_version.nsh @@ -1 +1 @@ -!define VERSION '3.11.1.3812' +!define VERSION '3.11.1.3814' diff --git a/TestSuite/_MachineLearning/Core/001_labels_to_ints_strict.pas b/TestSuite/_MachineLearning/Core/001_labels_to_ints_strict.pas new file mode 100644 index 000000000..728d157df --- /dev/null +++ b/TestSuite/_MachineLearning/Core/001_labels_to_ints_strict.pas @@ -0,0 +1,15 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var y := new Vector(Arr(0.0, 0.9999999999999, 2.0000000000001)); + var labels := LabelsToInts(y); + + Check(labels.Length = 3, 'Length mismatch'); + Check(labels[0] = 0, 'First label mismatch'); + Check(labels[1] = 1, 'Second label mismatch'); + Check(labels[2] = 2, 'Third label mismatch'); + + var bad := new Vector(Arr(0.0, 1.2, 2.0)); + CheckRaises(procedure -> begin var tmp := LabelsToInts(bad); end, 'LabelsToInts must reject non-integer labels'); +end. diff --git a/TestSuite/_MachineLearning/Core/002_vector_tointarray_strict.pas b/TestSuite/_MachineLearning/Core/002_vector_tointarray_strict.pas new file mode 100644 index 000000000..69d8b0873 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/002_vector_tointarray_strict.pas @@ -0,0 +1,15 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ok := new Vector(Arr(1.0, 1.9999999999999, -3.0000000000001)); + var a := ok.ToIntArray; + + Check(a.Length = 3, 'Length mismatch'); + Check(a[0] = 1, 'First value mismatch'); + Check(a[1] = 2, 'Second value mismatch'); + Check(a[2] = -3, 'Third value mismatch'); + + var bad := new Vector(Arr(1.2, 2.0)); + CheckRaises(procedure -> begin var tmp := bad.ToIntArray; end, 'ToIntArray must reject non-integer values'); +end. diff --git a/TestSuite/_MachineLearning/Core/003_dataframe_setschema_validation.pas b/TestSuite/_MachineLearning/Core/003_dataframe_setschema_validation.pas new file mode 100644 index 000000000..6eb200e1c --- /dev/null +++ b/TestSuite/_MachineLearning/Core/003_dataframe_setschema_validation.pas @@ -0,0 +1,15 @@ +uses MLABC, DataFrameABCCore; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('Msk', 'Spb')); + df.AddIntColumn('Age', Arr(20, 30)); + df := df.SetCategorical(['City']); + + var badName := new DataFrameSchema(Arr('Town', 'Age'), Arr(ColumnType.ctStr, ColumnType.ctInt), Arr(true, false)); + CheckRaises(procedure -> begin df.SetSchema(badName); end, 'SetSchema must reject name mismatch'); + + var badType := new DataFrameSchema(Arr('City', 'Age'), Arr(ColumnType.ctBool, ColumnType.ctInt), Arr(true, false)); + CheckRaises(procedure -> begin df.SetSchema(badType); end, 'SetSchema must reject type mismatch'); +end. diff --git a/TestSuite/_MachineLearning/Core/004_groupby_bool_key_schema.pas b/TestSuite/_MachineLearning/Core/004_groupby_bool_key_schema.pas new file mode 100644 index 000000000..7b7f9fa10 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/004_groupby_bool_key_schema.pas @@ -0,0 +1,17 @@ +uses MLABC, DataFrameABCCore; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddBoolColumn('Flag', Arr(true, false, true)); + df.AddFloatColumn('X', Arr(1.0, 2.0, 3.0)); + df := df.SetCategorical(['Flag']); + + var g := df.GroupBy('Flag').Sum('X'); + + Check(g.Schema.ColumnCount = 2, 'Unexpected column count'); + Check(g.Schema.NameAt(0) = 'Flag', 'Key column name mismatch'); + Check(g.Schema.ColumnTypeAt(0) = ColumnType.ctBool, 'Key column type must stay bool'); + Check(g.GetColumn(0).Info.Name = 'Flag', 'Physical key column name mismatch'); + Check(g.GetColumn(0).Info.ColType = ColumnType.ctBool, 'Physical key column type must stay bool'); +end. diff --git a/TestSuite/_MachineLearning/Core/005_head_tail_empty_schema.pas b/TestSuite/_MachineLearning/Core/005_head_tail_empty_schema.pas new file mode 100644 index 000000000..08b7734ed --- /dev/null +++ b/TestSuite/_MachineLearning/Core/005_head_tail_empty_schema.pas @@ -0,0 +1,28 @@ +uses MLABC, DataFrameABCCore; +uses TestHelpers in '..\TestHelpers.pas'; + +procedure CheckSameSchema(a, b: DataFrame; prefix: string); +begin + Check(a.Schema.ColumnCount = b.Schema.ColumnCount, prefix + ': column count mismatch'); + for var i := 0 to a.Schema.ColumnCount - 1 do + begin + Check(a.Schema.NameAt(i) = b.Schema.NameAt(i), prefix + $': name mismatch at {i}'); + Check(a.Schema.ColumnTypeAt(i) = b.Schema.ColumnTypeAt(i), prefix + $': type mismatch at {i}'); + Check(a.Schema.IsCategoricalAt(i) = b.Schema.IsCategoricalAt(i), prefix + $': categorical mismatch at {i}'); + end; +end; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('Msk', 'Spb', 'Kzn')); + df.AddIntColumn('Age', Arr(20, 30, 40)); + df := df.SetCategorical(['City']); + + var h := df.Head(0); + var t := df.Tail(0); + + Check(h.RowCount = 0, 'Head(0) must have 0 rows'); + Check(t.RowCount = 0, 'Tail(0) must have 0 rows'); + CheckSameSchema(df, h, 'Head(0)'); + CheckSameSchema(df, t, 'Tail(0)'); +end. diff --git a/TestSuite/_MachineLearning/Core/006_standardizeall_preserves_nonnumeric_types.pas b/TestSuite/_MachineLearning/Core/006_standardizeall_preserves_nonnumeric_types.pas new file mode 100644 index 000000000..7d71eb8ab --- /dev/null +++ b/TestSuite/_MachineLearning/Core/006_standardizeall_preserves_nonnumeric_types.pas @@ -0,0 +1,16 @@ +uses MLABC, DataFrameABCCore; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('Msk', 'Spb')); + df.AddIntColumn('Age', Arr(20, 30)); + + var z := Statistics.StandardizeAll(df); + + Check(z.Schema.ColumnCount = 2, 'Unexpected column count'); + Check(z.Schema.ColumnTypeAt(0) = ColumnType.ctStr, 'String column type must be preserved'); + Check(z.Schema.ColumnTypeAt(1) = ColumnType.ctFloat, 'Numeric column must become float'); + Check(z.GetColumn(0).Info.ColType = ColumnType.ctStr, 'Physical string column type must be preserved'); + Check(z.GetColumn(1).Info.ColType = ColumnType.ctFloat, 'Physical numeric column must become float'); +end. diff --git a/TestSuite/_MachineLearning/Core/007_normalizeall_preserves_nonnumeric_types.pas b/TestSuite/_MachineLearning/Core/007_normalizeall_preserves_nonnumeric_types.pas new file mode 100644 index 000000000..a8d468bb3 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/007_normalizeall_preserves_nonnumeric_types.pas @@ -0,0 +1,16 @@ +uses MLABC, DataFrameABCCore; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('Msk', 'Spb')); + df.AddIntColumn('Age', Arr(20, 30)); + + var z := Statistics.NormalizeAll(df); + + Check(z.Schema.ColumnCount = 2, 'Unexpected column count'); + Check(z.Schema.ColumnTypeAt(0) = ColumnType.ctStr, 'String column type must be preserved'); + Check(z.Schema.ColumnTypeAt(1) = ColumnType.ctFloat, 'Numeric column must become float'); + Check(z.GetColumn(0).Info.ColType = ColumnType.ctStr, 'Physical string column type must be preserved'); + Check(z.GetColumn(1).Info.ColType = ColumnType.ctFloat, 'Physical numeric column must become float'); +end. diff --git a/TestSuite/_MachineLearning/Core/008_transformlabels_unknown_class_raises.pas b/TestSuite/_MachineLearning/Core/008_transformlabels_unknown_class_raises.pas new file mode 100644 index 000000000..b9fc9e954 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/008_transformlabels_unknown_class_raises.pas @@ -0,0 +1,21 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var trainDf := new DataFrame; + trainDf.AddStrColumn('Target', Arr('cat', 'dog', 'cat')); + trainDf := trainDf.SetCategorical(['Target']); + + var testDf := new DataFrame; + testDf.AddStrColumn('Target', Arr('cat', 'fox')); + testDf := testDf.SetCategorical(['Target']); + + var classes: array of string; + var yTrain := trainDf.EncodeLabels('Target', classes); + + Check(yTrain.Length = 3, 'Encoded training labels length mismatch'); + Check(classes.Length = 2, 'Unexpected class count'); + + CheckRaises(procedure -> begin var tmp := testDf.TransformLabels('Target', classes); end, + 'TransformLabels must reject unseen target classes'); +end. diff --git a/TestSuite/_MachineLearning/Core/009_mape_rejects_nan_inf.pas b/TestSuite/_MachineLearning/Core/009_mape_rejects_nan_inf.pas new file mode 100644 index 000000000..7a891f37e --- /dev/null +++ b/TestSuite/_MachineLearning/Core/009_mape_rejects_nan_inf.pas @@ -0,0 +1,17 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var yTrue := new Vector(Arr(1.0, 2.0, 4.0)); + var good := new Vector(Arr(1.0, 2.2, 3.8)); + var m := Metrics.MAPE(yTrue, good); + Check(m >= 0.0, 'MAPE must be non-negative'); + + var withNaN := new Vector(Arr(1.0, real.NaN, 3.0)); + CheckRaises(procedure -> begin var tmp := Metrics.MAPE(yTrue, withNaN); end, + 'MAPE must reject NaN in predictions'); + + var withInf := new Vector(Arr(1.0, real.PositiveInfinity, 3.0)); + CheckRaises(procedure -> begin var tmp := Metrics.MAPE(yTrue, withInf); end, + 'MAPE must reject Infinity in predictions'); +end. diff --git a/TestSuite/_MachineLearning/Core/010_onehot_preserves_other_metadata.pas b/TestSuite/_MachineLearning/Core/010_onehot_preserves_other_metadata.pas new file mode 100644 index 000000000..f6d1917be --- /dev/null +++ b/TestSuite/_MachineLearning/Core/010_onehot_preserves_other_metadata.pas @@ -0,0 +1,22 @@ +uses MLABC, DataFrameABCCore; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('Msk', 'Spb', 'Kzn')); + df.AddStrColumn('Region', Arr('North', 'South', 'North')); + df.AddIntColumn('Age', Arr(20, 30, 40)); + df := df.SetCategorical(['City', 'Region']); + + var enc := new OneHotEncoder('Region'); + var res := enc.FitTransform(df); + + Check(res.HasColumn('City'), 'Untouched categorical column must stay in result'); + Check(not res.HasColumn('Region'), 'Source column must be removed after one-hot encoding'); + Check(res.Schema.IsCategorical('City'), 'Untouched categorical metadata must be preserved'); + Check(res.Schema.ColumnTypeAt(res.ColumnIndex('City')) = ColumnType.ctStr, 'Untouched column type must stay string'); + Check(res.HasColumn('Region_North'), 'First one-hot column missing'); + Check(res.HasColumn('Region_South'), 'Second one-hot column missing'); + Check(res.Schema.ColumnTypeAt(res.ColumnIndex('Region_North')) = ColumnType.ctInt, 'One-hot columns must be int'); + Check(not res.Schema.IsCategorical('Region_North'), 'One-hot columns must not be categorical'); +end. diff --git a/TestSuite/_MachineLearning/Core/011_imputer_preserves_schema.pas b/TestSuite/_MachineLearning/Core/011_imputer_preserves_schema.pas new file mode 100644 index 000000000..00269c321 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/011_imputer_preserves_schema.pas @@ -0,0 +1,20 @@ +uses MLABC, DataFrameABCCore; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('Msk', 'Spb', 'Kzn')); + df.AddIntColumn('Age', Arr(20, 0, 40), Arr(true, false, true)); + df := df.SetCategorical(['City']); + + var imp := new Imputer(['Age']); + var res := imp.FitTransform(df); + + Check(res.Schema.ColumnCount = df.Schema.ColumnCount, 'Schema column count must stay the same'); + Check(res.Schema.NameAt(0) = 'City', 'City name mismatch'); + Check(res.Schema.NameAt(1) = 'Age', 'Age name mismatch'); + Check(res.Schema.ColumnTypeAt(0) = ColumnType.ctStr, 'City type must stay string'); + Check(res.Schema.ColumnTypeAt(1) = ColumnType.ctFloat, 'Age type must become float for mean imputation'); + Check(res.Schema.IsCategoricalAt(0), 'City categorical flag must be preserved'); + Check(not res.Schema.IsCategoricalAt(1), 'Age categorical flag must stay false'); +end. diff --git a/TestSuite/_MachineLearning/Core/012_imputer_constant_preserves_int_type.pas b/TestSuite/_MachineLearning/Core/012_imputer_constant_preserves_int_type.pas new file mode 100644 index 000000000..e9193b853 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/012_imputer_constant_preserves_int_type.pas @@ -0,0 +1,20 @@ +uses MLABC, DataFrameABCCore; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddIntColumn('Age', Arr(20, 0, 40), Arr(true, false, true)); + + var imp := new Imputer(25, ['Age']); + var res := imp.FitTransform(df); + + Check(res.Schema.ColumnCount = 1, 'Unexpected column count'); + Check(res.Schema.NameAt(0) = 'Age', 'Column name mismatch'); + Check(res.Schema.ColumnTypeAt(0) = ColumnType.ctInt, 'Constant int imputation must preserve int type'); + Check(res.GetColumn(0).Info.ColType = ColumnType.ctInt, 'Physical column type must stay int'); + + var age := res.GetIntColumn('Age'); + Check(age[0] = 20, 'First value mismatch'); + Check(age[1] = 25, 'Imputed value mismatch'); + Check(age[2] = 40, 'Third value mismatch'); +end. diff --git a/TestSuite/_MachineLearning/Core/013_groupby_count_bool_key_schema.pas b/TestSuite/_MachineLearning/Core/013_groupby_count_bool_key_schema.pas new file mode 100644 index 000000000..5c5065040 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/013_groupby_count_bool_key_schema.pas @@ -0,0 +1,17 @@ +uses MLABC, DataFrameABCCore; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddBoolColumn('Flag', Arr(true, false, true)); + df.AddFloatColumn('X', Arr(1.0, 2.0, 3.0)); + df := df.SetCategorical(['Flag']); + + var g := df.GroupBy('Flag').Count; + + Check(g.Schema.ColumnCount = 2, 'Unexpected column count'); + Check(g.Schema.NameAt(0) = 'Flag', 'Key column name mismatch'); + Check(g.Schema.ColumnTypeAt(0) = ColumnType.ctBool, 'Key column type must stay bool'); + Check(g.GetColumn(0).Info.Name = 'Flag', 'Physical key column name mismatch'); + Check(g.GetColumn(0).Info.ColType = ColumnType.ctBool, 'Physical key column type must stay bool'); +end. diff --git a/TestSuite/_MachineLearning/Core/014_dataframe_cursor_invalid_accessor_raises.pas b/TestSuite/_MachineLearning/Core/014_dataframe_cursor_invalid_accessor_raises.pas new file mode 100644 index 000000000..778e54803 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/014_dataframe_cursor_invalid_accessor_raises.pas @@ -0,0 +1,15 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddIntColumn('Age', Arr(20, 30)); + + var cur := df.GetCursor; + Check(cur.MoveNext, 'Cursor must move to first row'); + + CheckRaises(procedure -> begin var s := cur.Str(0); end, + 'Cursor.Str on int column must raise'); + CheckRaises(procedure -> begin var b := cur.Bool(0); end, + 'Cursor.Bool on int column must raise'); +end. diff --git a/TestSuite/_MachineLearning/Core/015_transformlabels_unknown_int_class_raises.pas b/TestSuite/_MachineLearning/Core/015_transformlabels_unknown_int_class_raises.pas new file mode 100644 index 000000000..b878c13d6 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/015_transformlabels_unknown_int_class_raises.pas @@ -0,0 +1,21 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var trainDf := new DataFrame; + trainDf.AddIntColumn('Target', Arr(10, 20, 10)); + trainDf := trainDf.SetCategorical(['Target']); + + var testDf := new DataFrame; + testDf.AddIntColumn('Target', Arr(10, 30)); + testDf := testDf.SetCategorical(['Target']); + + var classes: array of string; + var yTrain := trainDf.EncodeLabels('Target', classes); + + Check(yTrain.Length = 3, 'Encoded training labels length mismatch'); + Check(classes.Length = 2, 'Unexpected class count'); + + CheckRaises(procedure -> begin var tmp := testDf.TransformLabels('Target', classes); end, + 'TransformLabels must reject unseen integer target classes'); +end. diff --git a/TestSuite/_MachineLearning/Core/016_mape_skips_zero_targets.pas b/TestSuite/_MachineLearning/Core/016_mape_skips_zero_targets.pas new file mode 100644 index 000000000..fbd8f3a3e --- /dev/null +++ b/TestSuite/_MachineLearning/Core/016_mape_skips_zero_targets.pas @@ -0,0 +1,13 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var yTrue := new Vector(Arr(0.0, 2.0, 4.0)); + var yPred := new Vector(Arr(100.0, 1.0, 5.0)); + + var m := Metrics.MAPE(yTrue, yPred); + + Check(not real.IsNaN(m), 'MAPE must not become NaN when yTrue contains zeros'); + Check(not real.IsInfinity(m), 'MAPE must not become Infinity when yTrue contains zeros'); + Check(Abs(m - 0.375) < 1e-12, 'MAPE must ignore zero targets in the average'); +end. diff --git a/TestSuite/_MachineLearning/Core/017_r2_zero_variance_returns_zero.pas b/TestSuite/_MachineLearning/Core/017_r2_zero_variance_returns_zero.pas new file mode 100644 index 000000000..d7298990b --- /dev/null +++ b/TestSuite/_MachineLearning/Core/017_r2_zero_variance_returns_zero.pas @@ -0,0 +1,10 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var yTrue := new Vector(Arr(5.0, 5.0, 5.0)); + var yPred := new Vector(Arr(5.0, 5.0, 5.0)); + + var r2 := Metrics.R2(yTrue, yPred); + Check(Abs(r2 - 0.0) < 1e-12, 'R2 must return 0.0 when target variance is zero'); +end. diff --git a/TestSuite/_MachineLearning/Core/018_head_regular_slice.pas b/TestSuite/_MachineLearning/Core/018_head_regular_slice.pas new file mode 100644 index 000000000..090415345 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/018_head_regular_slice.pas @@ -0,0 +1,16 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('A', 'B', 'C', 'D')); + df.AddIntColumn('Age', Arr(10, 20, 30, 40)); + + var h := df.Head(2); + + Check(h.RowCount = 2, 'Head(2) row count mismatch'); + Check(h.GetStrColumn('City')[0] = 'A', 'Head first row mismatch'); + Check(h.GetStrColumn('City')[1] = 'B', 'Head second row mismatch'); + Check(h.GetIntColumn('Age')[0] = 10, 'Head first age mismatch'); + Check(h.GetIntColumn('Age')[1] = 20, 'Head second age mismatch'); +end. diff --git a/TestSuite/_MachineLearning/Core/019_tail_regular_slice.pas b/TestSuite/_MachineLearning/Core/019_tail_regular_slice.pas new file mode 100644 index 000000000..d2403cca0 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/019_tail_regular_slice.pas @@ -0,0 +1,16 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('A', 'B', 'C', 'D')); + df.AddIntColumn('Age', Arr(10, 20, 30, 40)); + + var t := df.Tail(2); + + Check(t.RowCount = 2, 'Tail(2) row count mismatch'); + Check(t.GetStrColumn('City')[0] = 'C', 'Tail first row mismatch'); + Check(t.GetStrColumn('City')[1] = 'D', 'Tail second row mismatch'); + Check(t.GetIntColumn('Age')[0] = 30, 'Tail first age mismatch'); + Check(t.GetIntColumn('Age')[1] = 40, 'Tail second age mismatch'); +end. diff --git a/TestSuite/_MachineLearning/Core/020_filter_preserves_schema.pas b/TestSuite/_MachineLearning/Core/020_filter_preserves_schema.pas new file mode 100644 index 000000000..a991640fa --- /dev/null +++ b/TestSuite/_MachineLearning/Core/020_filter_preserves_schema.pas @@ -0,0 +1,15 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('A', 'B', 'C')); + df.AddIntColumn('Age', Arr(10, 20, 30)); + df.AddFloatColumn('Score', Arr(1.0, 2.0, 3.0)); + df := df.SetCategorical(['City']); + + var res := df.Filter(r -> r.Int('Age') >= 20); + + Check(res.RowCount = 2, 'Filtered row count mismatch'); + CheckSchemaMatchesColumns(res, Arr(true, false, false)); +end. diff --git a/TestSuite/_MachineLearning/Core/021_takerows_preserves_schema_and_order.pas b/TestSuite/_MachineLearning/Core/021_takerows_preserves_schema_and_order.pas new file mode 100644 index 000000000..b817d9c92 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/021_takerows_preserves_schema_and_order.pas @@ -0,0 +1,16 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('A', 'B', 'C')); + df.AddIntColumn('Age', Arr(10, 20, 30)); + df := df.SetCategorical(['City']); + + var res := df.TakeRows([2, 0]); + + Check(res.RowCount = 2, 'TakeRows row count mismatch'); + Check(res.GetStrColumn('City')[0] = 'C', 'First row order mismatch'); + Check(res.GetStrColumn('City')[1] = 'A', 'Second row order mismatch'); + CheckSchemaMatchesColumns(res, Arr(true, false)); +end. diff --git a/TestSuite/_MachineLearning/Core/022_select_preserves_order_and_metadata.pas b/TestSuite/_MachineLearning/Core/022_select_preserves_order_and_metadata.pas new file mode 100644 index 000000000..ffcfae121 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/022_select_preserves_order_and_metadata.pas @@ -0,0 +1,19 @@ +uses MLABC, DataFrameABCCore; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('A', 'B')); + df.AddIntColumn('Age', Arr(10, 20)); + df := df.SetCategorical(['City']); + + var res := df.Select(['Age', 'City']); + + Check(res.Schema.ColumnCount = 2, 'Column count mismatch'); + Check(res.Schema.NameAt(0) = 'Age', 'First selected column mismatch'); + Check(res.Schema.NameAt(1) = 'City', 'Second selected column mismatch'); + Check(res.Schema.ColumnTypeAt(0) = ColumnType.ctInt, 'Age type mismatch'); + Check(res.Schema.ColumnTypeAt(1) = ColumnType.ctStr, 'City type mismatch'); + Check(res.Schema.IsCategoricalAt(1), 'City categorical flag must be preserved'); + CheckSchemaMatchesColumns(res, Arr(false, true)); +end. diff --git a/TestSuite/_MachineLearning/Core/023_rename_keeps_schema_and_column_name_consistent.pas b/TestSuite/_MachineLearning/Core/023_rename_keeps_schema_and_column_name_consistent.pas new file mode 100644 index 000000000..6f51ee3b3 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/023_rename_keeps_schema_and_column_name_consistent.pas @@ -0,0 +1,16 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('A', 'B')); + df.AddIntColumn('Age', Arr(10, 20)); + + var res := df.Rename('Age', 'Years'); + + Check(not res.HasColumn('Age'), 'Old column name must disappear'); + Check(res.HasColumn('Years'), 'New column name must appear'); + Check(res.Schema.NameAt(1) = 'Years', 'Schema must contain new column name'); + Check(res.GetColumn(1).Info.Name = 'Years', 'Physical column name must match schema'); + CheckSchemaMatchesColumns(res, Arr(false, false)); +end. diff --git a/TestSuite/_MachineLearning/Core/024_join_no_collision_names_contract.pas b/TestSuite/_MachineLearning/Core/024_join_no_collision_names_contract.pas new file mode 100644 index 000000000..820a64ac9 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/024_join_no_collision_names_contract.pas @@ -0,0 +1,21 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var left := new DataFrame; + left.AddIntColumn('id', Arr(1, 2)); + left.AddStrColumn('name', Arr('A', 'B')); + + var right := new DataFrame; + right.AddIntColumn('id', Arr(1, 2)); + right.AddFloatColumn('score', Arr(10.0, 20.0)); + + var res := left.Join(right, 'id'); + + Check(res.Schema.ColumnCount = 3, 'Join column count mismatch'); + Check(res.Schema.NameAt(0) = 'id', 'First join column mismatch'); + Check(res.Schema.NameAt(1) = 'name', 'Second join column mismatch'); + Check(res.Schema.NameAt(2) = 'score', 'Right unique column must stay without prefix'); + Check(res.GetColumn(2).Info.Name = 'score', 'Physical right column name mismatch'); + CheckSchemaMatchesColumns(res, Arr(false, false, false)); +end. diff --git a/TestSuite/_MachineLearning/Core/025_join_collision_names_contract.pas b/TestSuite/_MachineLearning/Core/025_join_collision_names_contract.pas new file mode 100644 index 000000000..fa8f6483b --- /dev/null +++ b/TestSuite/_MachineLearning/Core/025_join_collision_names_contract.pas @@ -0,0 +1,21 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var left := new DataFrame; + left.AddIntColumn('id', Arr(1, 2)); + left.AddFloatColumn('feature', Arr(1.0, 2.0)); + + var right := new DataFrame; + right.AddIntColumn('id', Arr(1, 2)); + right.AddFloatColumn('feature', Arr(10.0, 20.0)); + + var res := left.Join(right, 'id'); + + Check(res.Schema.ColumnCount = 3, 'Join column count mismatch'); + Check(res.Schema.NameAt(0) = 'id', 'First join column mismatch'); + Check(res.Schema.NameAt(1) = 'feature', 'Left feature name mismatch'); + Check(res.Schema.NameAt(2) = 'right_feature', 'Right conflicting column must get prefix'); + Check(res.GetColumn(2).Info.Name = 'right_feature', 'Physical conflicting right column name mismatch'); + CheckSchemaMatchesColumns(res, Arr(false, false, false)); +end. diff --git a/TestSuite/_MachineLearning/Core/026_drop_preserves_order_and_metadata.pas b/TestSuite/_MachineLearning/Core/026_drop_preserves_order_and_metadata.pas new file mode 100644 index 000000000..f7da06f46 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/026_drop_preserves_order_and_metadata.pas @@ -0,0 +1,18 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('A', 'B')); + df.AddIntColumn('Age', Arr(10, 20)); + df.AddFloatColumn('Score', Arr(1.0, 2.0)); + df := df.SetCategorical(['City']); + + var res := df.Drop(['Score']); + + Check(res.ColumnCount = 2, 'Drop column count mismatch'); + Check(res.RowCount = 2, 'Drop row count mismatch'); + Check(res.Schema.NameAt(0) = 'City', 'First remaining column mismatch'); + Check(res.Schema.NameAt(1) = 'Age', 'Second remaining column mismatch'); + CheckSchemaMatchesColumns(res, Arr(true, false)); +end. diff --git a/TestSuite/_MachineLearning/Core/027_join_multikey_contract.pas b/TestSuite/_MachineLearning/Core/027_join_multikey_contract.pas new file mode 100644 index 000000000..d4ba3c1ba --- /dev/null +++ b/TestSuite/_MachineLearning/Core/027_join_multikey_contract.pas @@ -0,0 +1,23 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var left := new DataFrame; + left.AddIntColumn('k1', Arr(1, 1, 2)); + left.AddStrColumn('k2', Arr('A', 'B', 'A')); + left.AddFloatColumn('x', Arr(10.0, 20.0, 30.0)); + left := left.SetCategorical(['k2']); + + var right := new DataFrame; + right.AddIntColumn('k1', Arr(1, 2)); + right.AddStrColumn('k2', Arr('A', 'A')); + right.AddFloatColumn('y', Arr(100.0, 200.0)); + right := right.SetCategorical(['k2']); + + var res := left.Join(right, ['k1', 'k2']); + + Check(res.HasColumn('x'), 'Left payload column missing'); + Check(res.HasColumn('y'), 'Right payload column missing'); + Check(res.RowCount = 2, 'Multi-key join row count mismatch'); + CheckSchemaMatchesColumns(res); +end. diff --git a/TestSuite/_MachineLearning/Core/028_right_join_different_keys_contract.pas b/TestSuite/_MachineLearning/Core/028_right_join_different_keys_contract.pas new file mode 100644 index 000000000..083662000 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/028_right_join_different_keys_contract.pas @@ -0,0 +1,18 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var left := new DataFrame; + left.AddIntColumn('id_left', Arr(1, 2)); + left.AddStrColumn('name', Arr('A', 'B')); + + var right := new DataFrame; + right.AddIntColumn('id_right', Arr(2, 3)); + right.AddFloatColumn('score', Arr(20.0, 30.0)); + + var res := left.Join(right, ['id_left'], ['id_right'], jkRight); + + Check(res.HasColumn('score'), 'Right payload column missing'); + Check(res.RowCount = 2, 'Right join row count mismatch'); + CheckSchemaMatchesColumns(res); +end. diff --git a/TestSuite/_MachineLearning/Core/029_full_join_contract.pas b/TestSuite/_MachineLearning/Core/029_full_join_contract.pas new file mode 100644 index 000000000..2dea05aea --- /dev/null +++ b/TestSuite/_MachineLearning/Core/029_full_join_contract.pas @@ -0,0 +1,19 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var left := new DataFrame; + left.AddIntColumn('id', Arr(1, 2)); + left.AddStrColumn('name', Arr('A', 'B')); + + var right := new DataFrame; + right.AddIntColumn('id', Arr(2, 3)); + right.AddFloatColumn('score', Arr(20.0, 30.0)); + + var res := left.Join(right, 'id', jkFull); + + Check(res.RowCount = 3, 'Full join row count mismatch'); + Check(res.HasColumn('name'), 'Left payload column missing'); + Check(res.HasColumn('score'), 'Right payload column missing'); + CheckSchemaMatchesColumns(res); +end. diff --git a/TestSuite/_MachineLearning/Core/030_labelencoder_first_appearance.pas b/TestSuite/_MachineLearning/Core/030_labelencoder_first_appearance.pas new file mode 100644 index 000000000..ac12ca922 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/030_labelencoder_first_appearance.pas @@ -0,0 +1,19 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('Region', Arr('South', 'North', 'South', 'East')); + df := df.SetCategorical(['Region']); + + var enc := new LabelEncoder('Region'); + enc.Fit(df); + + var res := enc.Transform(df); + var labels := res.GetIntColumn('Region'); + + Check(labels[0] = 0, 'First appearance of South must get code 0'); + Check(labels[1] = 1, 'First appearance of North must get code 1'); + Check(labels[2] = 0, 'Repeated South must keep code 0'); + Check(labels[3] = 2, 'First appearance of East must get code 2'); +end. diff --git a/TestSuite/_MachineLearning/Core/031_dataadapters_encode_str_contract.pas b/TestSuite/_MachineLearning/Core/031_dataadapters_encode_str_contract.pas new file mode 100644 index 000000000..3bd47aa3e --- /dev/null +++ b/TestSuite/_MachineLearning/Core/031_dataadapters_encode_str_contract.pas @@ -0,0 +1,25 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('Target', Arr('cat', 'dog', 'cat', 'fox')); + df := df.SetCategorical(['Target']); + + var classes: array of string; + var y := df.EncodeLabels('Target', classes); + + Check(y.Length = 4, 'Encoded label length mismatch'); + Check(classes.Length = 3, 'Class count mismatch'); + Check(classes[0] = 'cat', 'First class must follow first appearance'); + Check(classes[1] = 'dog', 'Second class must follow first appearance'); + Check(classes[2] = 'fox', 'Third class must follow first appearance'); + Check(y[0] = 0, 'First encoded label mismatch'); + Check(y[1] = 1, 'Second encoded label mismatch'); + Check(y[2] = 0, 'Third encoded label mismatch'); + Check(y[3] = 2, 'Fourth encoded label mismatch'); + + var y2 := df.TransformLabels('Target', classes); + for var i := 0 to y.Length - 1 do + Check(y2[i] = y[i], $'TransformLabels mismatch at {i}'); +end. diff --git a/TestSuite/_MachineLearning/Core/032_dataadapters_encode_int_contract.pas b/TestSuite/_MachineLearning/Core/032_dataadapters_encode_int_contract.pas new file mode 100644 index 000000000..d59fb2760 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/032_dataadapters_encode_int_contract.pas @@ -0,0 +1,25 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddIntColumn('Target', Arr(10, 20, 10, 30)); + df := df.SetCategorical(['Target']); + + var classes: array of string; + var y := df.EncodeLabels('Target', classes); + + Check(y.Length = 4, 'Encoded label length mismatch'); + Check(classes.Length = 3, 'Class count mismatch'); + Check(classes[0] = '10', 'First class must follow first appearance'); + Check(classes[1] = '20', 'Second class must follow first appearance'); + Check(classes[2] = '30', 'Third class must follow first appearance'); + Check(y[0] = 0, 'First encoded label mismatch'); + Check(y[1] = 1, 'Second encoded label mismatch'); + Check(y[2] = 0, 'Third encoded label mismatch'); + Check(y[3] = 2, 'Fourth encoded label mismatch'); + + var y2 := df.TransformLabels('Target', classes); + for var i := 0 to y.Length - 1 do + Check(y2[i] = y[i], $'TransformLabels mismatch at {i}'); +end. diff --git a/TestSuite/_MachineLearning/Core/033_full_join_different_keys_contract.pas b/TestSuite/_MachineLearning/Core/033_full_join_different_keys_contract.pas new file mode 100644 index 000000000..f86e74926 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/033_full_join_different_keys_contract.pas @@ -0,0 +1,19 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var left := new DataFrame; + left.AddIntColumn('id_left', Arr(1, 2)); + left.AddStrColumn('name', Arr('A', 'B')); + + var right := new DataFrame; + right.AddIntColumn('id_right', Arr(2, 3)); + right.AddFloatColumn('score', Arr(20.0, 30.0)); + + var res := left.Join(right, ['id_left'], ['id_right'], jkFull); + + Check(res.RowCount = 3, 'Full join with different keys row count mismatch'); + Check(res.HasColumn('name'), 'Left payload column missing'); + Check(res.HasColumn('score'), 'Right payload column missing'); + CheckSchemaMatchesColumns(res); +end. diff --git a/TestSuite/_MachineLearning/Core/034_dataframe_setschema_failure_is_atomic.pas b/TestSuite/_MachineLearning/Core/034_dataframe_setschema_failure_is_atomic.pas new file mode 100644 index 000000000..c360b593a --- /dev/null +++ b/TestSuite/_MachineLearning/Core/034_dataframe_setschema_failure_is_atomic.pas @@ -0,0 +1,21 @@ +uses MLABC, DataFrameABCCore; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('Msk', 'Spb')); + df.AddIntColumn('Age', Arr(20, 30)); + df := df.SetCategorical(['City']); + + CheckRaises(procedure -> begin + df.SetSchema(new DataFrameSchema( + Arr('Town', 'Age'), + Arr(ColumnType.ctStr, ColumnType.ctInt), + Arr(true, false) + )); + end, 'SetSchema must reject mismatched schema'); + + Check(df.ColumnCount = 2, 'ColumnCount must stay unchanged after failed SetSchema'); + Check(df.Schema.NameAt(0) = 'City', 'Schema must stay unchanged after failed SetSchema'); + CheckSchemaMatchesColumns(df, Arr(true, false)); +end. diff --git a/TestSuite/_MachineLearning/Core/035_dataframe_add_duplicate_column_is_atomic.pas b/TestSuite/_MachineLearning/Core/035_dataframe_add_duplicate_column_is_atomic.pas new file mode 100644 index 000000000..1b883cf7f --- /dev/null +++ b/TestSuite/_MachineLearning/Core/035_dataframe_add_duplicate_column_is_atomic.pas @@ -0,0 +1,15 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddIntColumn('Age', Arr(10, 20, 30)); + + CheckRaises(procedure -> begin + df.AddIntColumn('Age', Arr(1, 2, 3)); + end, 'Duplicate AddIntColumn must be rejected'); + + Check(df.ColumnCount = 1, 'ColumnCount must stay unchanged after duplicate AddIntColumn'); + Check(df.HasColumn('Age'), 'Original column must stay present'); + CheckSchemaMatchesColumns(df, Arr(false)); +end. diff --git a/TestSuite/_MachineLearning/Core/036_setcategorical_returns_independent_dataframe.pas b/TestSuite/_MachineLearning/Core/036_setcategorical_returns_independent_dataframe.pas new file mode 100644 index 000000000..8d84802c3 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/036_setcategorical_returns_independent_dataframe.pas @@ -0,0 +1,18 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('Msk', 'Spb')); + df.AddIntColumn('Age', Arr(20, 30)); + + var df2 := df.SetCategorical(['City']); + df.AddIntColumn('X', Arr(1, 2)); + + Check(df.ColumnCount = 3, 'Original DataFrame must have the new column'); + Check(df2.ColumnCount = 2, 'SetCategorical result must keep its own column list'); + Check(not df2.HasColumn('X'), 'SetCategorical result must not see later columns from source DataFrame'); + Check(df2.Schema.NameAt(0) = 'City', 'First schema name must stay intact'); + Check(df2.Schema.NameAt(1) = 'Age', 'Second schema name must stay intact'); + CheckSchemaMatchesColumns(df2, Arr(true, false)); +end. diff --git a/TestSuite/_MachineLearning/Core/037_metrics_accuracy_rejects_non_integer_labels.pas b/TestSuite/_MachineLearning/Core/037_metrics_accuracy_rejects_non_integer_labels.pas new file mode 100644 index 000000000..4aeda3a2d --- /dev/null +++ b/TestSuite/_MachineLearning/Core/037_metrics_accuracy_rejects_non_integer_labels.pas @@ -0,0 +1,11 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var yTrue := new Vector(Arr(0.49, 1.0)); + var yPred := new Vector(Arr(0.51, 1.0)); + + CheckRaises(procedure -> begin + var a := Metrics.Accuracy(yTrue, yPred); + end, 'Accuracy must reject non-integer class labels'); +end. diff --git a/TestSuite/_MachineLearning/Core/038_metrics_accuracy_rejects_nan_inf.pas b/TestSuite/_MachineLearning/Core/038_metrics_accuracy_rejects_nan_inf.pas new file mode 100644 index 000000000..b9c041ebc --- /dev/null +++ b/TestSuite/_MachineLearning/Core/038_metrics_accuracy_rejects_nan_inf.pas @@ -0,0 +1,11 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var yTrue := new Vector(Arr(0.0, 1.0)); + var yPred := new Vector(Arr(real.NaN, 1.0)); + + CheckRaises(procedure -> begin + var a := Metrics.Accuracy(yTrue, yPred); + end, 'Accuracy must reject NaN and Infinity in class labels'); +end. diff --git a/TestSuite/_MachineLearning/Core/039_join_preserves_categorical_flags_on_right_nonkey_columns.pas b/TestSuite/_MachineLearning/Core/039_join_preserves_categorical_flags_on_right_nonkey_columns.pas new file mode 100644 index 000000000..22a8256ab --- /dev/null +++ b/TestSuite/_MachineLearning/Core/039_join_preserves_categorical_flags_on_right_nonkey_columns.pas @@ -0,0 +1,19 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var left := new DataFrame; + left.AddIntColumn('Id', Arr(1, 2)); + left.AddStrColumn('Name', Arr('A', 'B')); + + var right := new DataFrame; + right.AddIntColumn('Id', Arr(1, 2)); + right.AddStrColumn('City', Arr('Msk', 'Spb')); + right := right.SetCategorical(['City']); + + var joined := left.Join(right, 'Id'); + + Check(joined.HasColumn('City'), 'Joined DataFrame must contain right non-key column City'); + Check(joined.IsCategorical('City'), 'Right categorical non-key column must stay categorical after Join'); + CheckSchemaMatchesColumns(joined, Arr(false, false, true)); +end. diff --git a/TestSuite/_MachineLearning/Core/040_groupby_keys_are_marked_categorical.pas b/TestSuite/_MachineLearning/Core/040_groupby_keys_are_marked_categorical.pas new file mode 100644 index 000000000..78210e66d --- /dev/null +++ b/TestSuite/_MachineLearning/Core/040_groupby_keys_are_marked_categorical.pas @@ -0,0 +1,14 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddBoolColumn('Flag', Arr(true, false, true)); + df.AddFloatColumn('X', Arr(1.0, 2.0, 3.0)); + df := df.SetCategorical(['Flag']); + + var g := df.GroupBy(['Flag']).Count; + + Check(g.IsCategorical('Flag'), 'GroupBy key must stay categorical'); + CheckSchemaMatchesColumns(g, Arr(true, false)); +end. diff --git a/TestSuite/_MachineLearning/Core/041_labelencoder_transform_type_drift_raises_contract_error.pas b/TestSuite/_MachineLearning/Core/041_labelencoder_transform_type_drift_raises_contract_error.pas new file mode 100644 index 000000000..56ee4226a --- /dev/null +++ b/TestSuite/_MachineLearning/Core/041_labelencoder_transform_type_drift_raises_contract_error.pas @@ -0,0 +1,19 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var trainDf := new DataFrame; + trainDf.AddStrColumn('City', Arr('Msk', 'Spb', 'Msk')); + trainDf := trainDf.SetCategorical(['City']); + + var testDf := new DataFrame; + testDf.AddIntColumn('City', Arr(1, 2, 3)); + testDf := testDf.SetCategorical(['City']); + + var enc := new LabelEncoder('City'); + enc.Fit(trainDf); + + CheckRaises(procedure -> begin + var res := enc.Transform(testDf); + end, 'LabelEncoder must raise a clear contract error on column type drift'); +end. diff --git a/TestSuite/_MachineLearning/Core/042_onehot_transform_type_drift_raises_contract_error.pas b/TestSuite/_MachineLearning/Core/042_onehot_transform_type_drift_raises_contract_error.pas new file mode 100644 index 000000000..51abc08e7 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/042_onehot_transform_type_drift_raises_contract_error.pas @@ -0,0 +1,19 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var trainDf := new DataFrame; + trainDf.AddStrColumn('City', Arr('Msk', 'Spb', 'Msk')); + trainDf := trainDf.SetCategorical(['City']); + + var testDf := new DataFrame; + testDf.AddIntColumn('City', Arr(1, 2, 3)); + testDf := testDf.SetCategorical(['City']); + + var enc := new OneHotEncoder('City'); + enc.Fit(trainDf); + + CheckRaises(procedure -> begin + var res := enc.Transform(testDf); + end, 'OneHotEncoder must raise a clear contract error on column type drift'); +end. diff --git a/TestSuite/_MachineLearning/Core/043_join_different_keys_with_right_name_collision.pas b/TestSuite/_MachineLearning/Core/043_join_different_keys_with_right_name_collision.pas new file mode 100644 index 000000000..7452f5e39 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/043_join_different_keys_with_right_name_collision.pas @@ -0,0 +1,20 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var left := new DataFrame; + left.AddIntColumn('left_id', Arr(1, 2)); + left.AddStrColumn('Name', Arr('A', 'B')); + + var right := new DataFrame; + right.AddIntColumn('right_id', Arr(1, 2)); + right.AddStrColumn('left_id', Arr('X', 'Y')); + right := right.SetCategorical(['left_id']); + + var joined := left.Join(right, Arr($'left_id'), Arr($'right_id'), jkInner); + + Check(joined.HasColumn('left_id'), 'Joined DataFrame must keep left key column'); + Check(joined.HasColumn('right_left_id'), 'Joined DataFrame must rename conflicting right non-key column'); + Check(joined.IsCategorical('right_left_id'), 'Renamed right categorical non-key column must stay categorical'); + CheckSchemaMatchesColumns(joined, Arr(false, false, true)); +end. diff --git a/TestSuite/_MachineLearning/Examples/001_iris_07_pipeline_smoke.pas b/TestSuite/_MachineLearning/Examples/001_iris_07_pipeline_smoke.pas new file mode 100644 index 000000000..25a7a52db --- /dev/null +++ b/TestSuite/_MachineLearning/Examples/001_iris_07_pipeline_smoke.pas @@ -0,0 +1,27 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var df := ds.Data; + + var pipe := + DataPipeline.Build( + TaskKind.tkClassification, + ds.Target, + ds.Features, + new StandardScaler, + new LogisticRegression + ); + + var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 3); + + pipe.Fit(trainDf); + + var pred := pipe.Predict(testDf); + var y := pipe.GetEncodedLabels(testDf); + + Check(pred.Length = testDf.RowCount, 'Predict length mismatch'); + Check(y.Length = testDf.RowCount, 'Encoded labels length mismatch'); + Check(Metrics.Accuracy(y, pred) > 0.8, 'Pipeline accuracy is unexpectedly low'); +end. diff --git a/TestSuite/_MachineLearning/Examples/002_iris_04_traintestsplit_smoke.pas b/TestSuite/_MachineLearning/Examples/002_iris_04_traintestsplit_smoke.pas new file mode 100644 index 000000000..f16cb4cd8 --- /dev/null +++ b/TestSuite/_MachineLearning/Examples/002_iris_04_traintestsplit_smoke.pas @@ -0,0 +1,20 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var X := ds.Data.ToMatrix(ds.Features); + + var classes: array of string; + var y := ds.Data.EncodeLabels(ds.Target, classes); + + var (Xtrain, Xtest, ytrain, ytest) := Validation.TrainTestSplit(X, y, 0.2, 1); + + var model := new LogisticRegression; + model.Fit(Xtrain, ytrain); + + var pred := model.Predict(Xtest); + + Check(pred.Length = ytest.Length, 'Prediction length mismatch'); + Check(Metrics.Accuracy(ytest, pred) > 0.8, 'TrainTestSplit example accuracy is unexpectedly low'); +end. diff --git a/TestSuite/_MachineLearning/Examples/003_preprocessing_minmax_smoke.pas b/TestSuite/_MachineLearning/Examples/003_preprocessing_minmax_smoke.pas new file mode 100644 index 000000000..bca0aad07 --- /dev/null +++ b/TestSuite/_MachineLearning/Examples/003_preprocessing_minmax_smoke.pas @@ -0,0 +1,17 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var X := new Matrix(5, 2); + X[0,0] := 1; X[1,0] := 2; X[2,0] := 3; X[3,0] := 10; X[4,0] := 20; + X[0,1] := 100; X[1,1] := 120; X[2,1] := 150; X[3,1] := 300; X[4,1] := 500; + + var scaler := new MinMaxScaler; + scaler.Fit(X); + var Xscaled := scaler.Transform(X); + + Check(Abs(Xscaled.ColumnMin(0) - 0.0) < 1e-12, 'First column min must be 0'); + Check(Abs(Xscaled.ColumnMax(0) - 1.0) < 1e-12, 'First column max must be 1'); + Check(Abs(Xscaled.ColumnMin(1) - 0.0) < 1e-12, 'Second column min must be 0'); + Check(Abs(Xscaled.ColumnMax(1) - 1.0) < 1e-12, 'Second column max must be 1'); +end. diff --git a/TestSuite/_MachineLearning/Examples/004_preprocessing_onehot_smoke.pas b/TestSuite/_MachineLearning/Examples/004_preprocessing_onehot_smoke.pas new file mode 100644 index 000000000..822470c6a --- /dev/null +++ b/TestSuite/_MachineLearning/Examples/004_preprocessing_onehot_smoke.pas @@ -0,0 +1,18 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('A', 'B', 'C')); + df.AddStrColumn('Region', Arr('North', 'South', 'North')); + df := df.SetCategorical(['City', 'Region']); + + var enc := new OneHotEncoder('Region'); + var res := enc.FitTransform(df); + + Check(res.HasColumn('City'), 'City must stay in result'); + Check(res.HasColumn('Region_North'), 'Region_North must exist'); + Check(res.HasColumn('Region_South'), 'Region_South must exist'); + Check(not res.HasColumn('Region'), 'Source region column must be removed'); + Check(res.RowCount = df.RowCount, 'Row count must stay the same'); +end. diff --git a/TestSuite/_MachineLearning/Examples/005_preprocessing_imputer_smoke.pas b/TestSuite/_MachineLearning/Examples/005_preprocessing_imputer_smoke.pas new file mode 100644 index 000000000..348b78c25 --- /dev/null +++ b/TestSuite/_MachineLearning/Examples/005_preprocessing_imputer_smoke.pas @@ -0,0 +1,16 @@ +uses MLABC, DataFrameABCCore; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('A', 'B', 'C')); + df.AddIntColumn('Population', Arr(10, 0, 30), Arr(true, false, true)); + + var imp := new Imputer(['Population']); + var res := imp.FitTransform(df); + + Check(res.RowCount = df.RowCount, 'Row count must stay the same'); + Check(res.HasColumn('City'), 'City must stay in result'); + Check(res.HasColumn('Population'), 'Population must stay in result'); + Check(res.GetColumn(res.ColumnIndex('Population')).Info.ColType = ColumnType.ctFloat, 'Mean-imputed population must become float'); +end. diff --git a/TestSuite/_MachineLearning/Examples/006_upipeline_kmeans_smoke.pas b/TestSuite/_MachineLearning/Examples/006_upipeline_kmeans_smoke.pas new file mode 100644 index 000000000..3494ef0be --- /dev/null +++ b/TestSuite/_MachineLearning/Examples/006_upipeline_kmeans_smoke.pas @@ -0,0 +1,17 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + + var pipe := + UDataPipeline.Build( + ds.Features, + new StandardScaler, + new KMeans(3, seed := 1) + ); + + var labels := pipe.FitPredict(ds.Data); + + Check(labels.Length = ds.Data.RowCount, 'UDataPipeline labels length mismatch'); +end. diff --git a/TestSuite/_MachineLearning/Regression/001_join_schema_names.pas b/TestSuite/_MachineLearning/Regression/001_join_schema_names.pas new file mode 100644 index 000000000..f3c4923b7 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/001_join_schema_names.pas @@ -0,0 +1,29 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var left1 := new DataFrame; + left1.AddIntColumn('id', Arr(1, 2)); + left1.AddIntColumn('feature1', Arr(10, 20)); + + var right1 := new DataFrame; + right1.AddIntColumn('id', Arr(1, 2)); + right1.AddIntColumn('feature2', Arr(100, 200)); + + var joined1 := left1.Join(right1, 'id'); + Check(joined1.HasColumn('feature2'), 'Expected feature2 without prefix'); + Check(not joined1.HasColumn('right_feature2'), 'Unexpected right_feature2 without collision'); + CheckSchemaMatchesColumns(joined1); + + var left2 := new DataFrame; + left2.AddIntColumn('id', Arr(1, 2)); + left2.AddIntColumn('feature', Arr(10, 20)); + + var right2 := new DataFrame; + right2.AddIntColumn('id', Arr(1, 2)); + right2.AddIntColumn('feature', Arr(100, 200)); + + var joined2 := left2.Join(right2, 'id'); + Check(joined2.HasColumn('right_feature'), 'Expected right_feature on collision'); + CheckSchemaMatchesColumns(joined2); +end. diff --git a/TestSuite/_MachineLearning/Regression/002_pipeline_predictlabels.pas b/TestSuite/_MachineLearning/Regression/002_pipeline_predictlabels.pas new file mode 100644 index 000000000..11078f51d --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/002_pipeline_predictlabels.pas @@ -0,0 +1,44 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var df := ds.Data; + var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 3); + + var pipe := + DataPipeline.Build( + TaskKind.tkClassification, + ds.Target, + ds.Features, + new StandardScaler, + new LogisticRegression + ); + + pipe.Fit(trainDf); + + var classes := pipe.GetClassLabels; + var pred := pipe.Predict(testDf); + var predLabels := pipe.PredictLabels(testDf); + var y := pipe.GetEncodedLabels(testDf); + var trueLabels := testDf.GetStrColumn(ds.Target); + + Check(classes.Length > 0, 'classes must not be empty'); + Check(pred.Length = testDf.RowCount, 'Predict length mismatch'); + Check(predLabels.Length = testDf.RowCount, 'PredictLabels length mismatch'); + Check(y.Length = testDf.RowCount, 'GetEncodedLabels length mismatch'); + + for var i := 0 to testDf.RowCount - 1 do + begin + var pi := Round(pred[i]); + var yi := Round(y[i]); + + Check(Abs(pred[i] - pi) < 1e-12, $'Predict[{i}] is not an encoded integer'); + Check((pi >= 0) and (pi < classes.Length), $'Predict[{i}] out of range'); + Check(predLabels[i] = classes[pi], $'PredictLabels[{i}] does not decode Predict[{i}]'); + + Check(Abs(y[i] - yi) < 1e-12, $'GetEncodedLabels[{i}] is not an encoded integer'); + Check((yi >= 0) and (yi < classes.Length), $'GetEncodedLabels[{i}] out of range'); + Check(classes[yi] = trueLabels[i], $'GetEncodedLabels[{i}] does not decode to true target'); + end; +end. diff --git a/TestSuite/_MachineLearning/Regression/003_logistic_predictlabels_contract.pas b/TestSuite/_MachineLearning/Regression/003_logistic_predictlabels_contract.pas new file mode 100644 index 000000000..de7b6da52 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/003_logistic_predictlabels_contract.pas @@ -0,0 +1,29 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var X := ds.Data.ToMatrix(ds.Features); + + var classes: array of string; + var y := ds.Data.EncodeLabels(ds.Target, classes); + + var model := new LogisticRegression; + model.Fit(X, y); + + var pred := model.Predict(X); + var labels := model.PredictLabels(X); + var modelClasses := model.GetClassLabels; + + Check(modelClasses.Length = classes.Length, 'class count mismatch'); + Check(pred.Length = X.RowCount, 'Predict length mismatch'); + Check(labels.Length = X.RowCount, 'PredictLabels length mismatch'); + + for var i := 0 to X.RowCount - 1 do + begin + var pi := Round(pred[i]); + Check(Abs(pred[i] - pi) < 1e-12, $'Predict[{i}] is not an original integer label'); + Check((labels[i] >= 0) and (labels[i] < modelClasses.Length), $'PredictLabels[{i}] out of range'); + Check(modelClasses[labels[i]] = pi.ToString, $'Predict[{i}] and PredictLabels[{i}] are inconsistent'); + end; +end. diff --git a/TestSuite/_MachineLearning/Regression/004_randomforest_predictlabels_contract.pas b/TestSuite/_MachineLearning/Regression/004_randomforest_predictlabels_contract.pas new file mode 100644 index 000000000..1491cb531 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/004_randomforest_predictlabels_contract.pas @@ -0,0 +1,29 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var X := ds.Data.ToMatrix(ds.Features); + + var classes: array of string; + var y := ds.Data.EncodeLabels(ds.Target, classes); + + var model := new RandomForestClassifier(20, maxDepth := 6, seed := 1); + model.Fit(X, y); + + var pred := model.Predict(X); + var labels := model.PredictLabels(X); + var modelClasses := model.GetClassLabels; + + Check(modelClasses.Length = classes.Length, 'class count mismatch'); + Check(pred.Length = X.RowCount, 'Predict length mismatch'); + Check(labels.Length = X.RowCount, 'PredictLabels length mismatch'); + + for var i := 0 to X.RowCount - 1 do + begin + var pi := Round(pred[i]); + Check(Abs(pred[i] - pi) < 1e-12, $'Predict[{i}] is not an original integer label'); + Check((labels[i] >= 0) and (labels[i] < modelClasses.Length), $'PredictLabels[{i}] out of range'); + Check(modelClasses[labels[i]] = pi.ToString, $'Predict[{i}] and PredictLabels[{i}] are inconsistent'); + end; +end. diff --git a/TestSuite/_MachineLearning/Regression/005_gradientboosting_predictlabels_contract.pas b/TestSuite/_MachineLearning/Regression/005_gradientboosting_predictlabels_contract.pas new file mode 100644 index 000000000..9e615a1d8 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/005_gradientboosting_predictlabels_contract.pas @@ -0,0 +1,29 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var X := ds.Data.ToMatrix(ds.Features); + + var classes: array of string; + var y := ds.Data.EncodeLabels(ds.Target, classes); + + var model := new GradientBoostingClassifier(20, learningRate := 0.1, maxDepth := 3, seed := 1); + model.Fit(X, y); + + var pred := model.Predict(X); + var labels := model.PredictLabels(X); + var modelClasses := model.GetClassLabels; + + Check(modelClasses.Length = classes.Length, 'class count mismatch'); + Check(pred.Length = X.RowCount, 'Predict length mismatch'); + Check(labels.Length = X.RowCount, 'PredictLabels length mismatch'); + + for var i := 0 to X.RowCount - 1 do + begin + var pi := Round(pred[i]); + Check(Abs(pred[i] - pi) < 1e-12, $'Predict[{i}] is not an original integer label'); + Check((labels[i] >= 0) and (labels[i] < modelClasses.Length), $'PredictLabels[{i}] out of range'); + Check(modelClasses[labels[i]] = pi.ToString, $'Predict[{i}] and PredictLabels[{i}] are inconsistent'); + end; +end. diff --git a/TestSuite/_MachineLearning/Regression/006_knn_predictlabels_contract.pas b/TestSuite/_MachineLearning/Regression/006_knn_predictlabels_contract.pas new file mode 100644 index 000000000..b526b13d6 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/006_knn_predictlabels_contract.pas @@ -0,0 +1,29 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var X := ds.Data.ToMatrix(ds.Features); + + var classes: array of string; + var y := ds.Data.EncodeLabels(ds.Target, classes); + + var model := new KNNClassifier(5); + model.Fit(X, y); + + var pred := model.Predict(X); + var labels := model.PredictLabels(X); + var modelClasses := model.GetClassLabels; + + Check(modelClasses.Length = classes.Length, 'class count mismatch'); + Check(pred.Length = X.RowCount, 'Predict length mismatch'); + Check(labels.Length = X.RowCount, 'PredictLabels length mismatch'); + + for var i := 0 to X.RowCount - 1 do + begin + var pi := Round(pred[i]); + Check(Abs(pred[i] - pi) < 1e-12, $'Predict[{i}] is not an original integer label'); + Check((labels[i] >= 0) and (labels[i] < modelClasses.Length), $'PredictLabels[{i}] out of range'); + Check(modelClasses[labels[i]] = pi.ToString, $'Predict[{i}] and PredictLabels[{i}] are inconsistent'); + end; +end. diff --git a/TestSuite/_MachineLearning/Regression/007_minmax_inverse_range.pas b/TestSuite/_MachineLearning/Regression/007_minmax_inverse_range.pas new file mode 100644 index 000000000..68f05d1a7 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/007_minmax_inverse_range.pas @@ -0,0 +1,20 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var X := new Matrix(4, 2); + X[0,0] := 1.0; X[0,1] := 10.0; + X[1,0] := 2.0; X[1,1] := 20.0; + X[2,0] := 5.0; X[2,1] := 40.0; + X[3,0] := 9.0; X[3,1] := 80.0; + + var scaler := new MinMaxScaler(-1.0, 1.0); + scaler.Fit(X); + + var scaled := scaler.Transform(X); + var restored := scaler.InverseTransform(scaled); + + for var i := 0 to X.RowCount - 1 do + for var j := 0 to X.ColCount - 1 do + Check(Abs(restored[i,j] - X[i,j]) < 1e-9, $'InverseTransform mismatch at [{i},{j}]'); +end. diff --git a/TestSuite/_MachineLearning/Regression/008_dataframe_schema_snapshot.pas b/TestSuite/_MachineLearning/Regression/008_dataframe_schema_snapshot.pas new file mode 100644 index 000000000..b16f7ffb9 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/008_dataframe_schema_snapshot.pas @@ -0,0 +1,24 @@ +uses MLABC, DataFrameABCCore; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddStrColumn('City', Arr('Msk', 'Spb')); + df.AddIntColumn('Age', Arr(20, 30)); + df := df.SetCategorical(['City']); + + var s1 := df.Schema; + var names := s1.ColumnNames; + var types := s1.Types; + var cats := s1.CategoricalFlags; + + names[0] := 'Broken'; + types[0] := ColumnType.ctBool; + cats[0] := false; + + var s2 := df.Schema; + + Check(s2.NameAt(0) = 'City', 'Schema name snapshot must not mutate DataFrame'); + Check(s2.ColumnTypeAt(0) = ColumnType.ctStr, 'Schema type snapshot must not mutate DataFrame'); + Check(s2.IsCategoricalAt(0), 'Schema categorical snapshot must not mutate DataFrame'); +end. diff --git a/TestSuite/_MachineLearning/Regression/009_pipeline_rowcount_invariant.pas b/TestSuite/_MachineLearning/Regression/009_pipeline_rowcount_invariant.pas new file mode 100644 index 000000000..94c51dded --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/009_pipeline_rowcount_invariant.pas @@ -0,0 +1,45 @@ +uses MLABC, PreprocessorABC; +uses TestHelpers in '..\TestHelpers.pas'; + +type + BadDropper = class(IPreprocessor) + public + function Fit(df: DataFrame): IPreprocessor; + function Transform(df: DataFrame): DataFrame; + function FitTransform(df: DataFrame): DataFrame; + function Clone: IPreprocessor; + end; + +function BadDropper.Fit(df: DataFrame): IPreprocessor; +begin + Result := Self; +end; + +function BadDropper.Transform(df: DataFrame): DataFrame; +begin + Result := df.Head(df.RowCount - 1); +end; + +function BadDropper.FitTransform(df: DataFrame): DataFrame; +begin + Result := Transform(df); +end; + +function BadDropper.Clone: IPreprocessor; +begin + Result := new BadDropper; +end; + +begin + var ds := Datasets.Iris; + var pipe := DataPipeline.Build( + TaskKind.tkClassification, + ds.Target, + ds.Features, + new BadDropper, + new LogisticRegression + ); + + CheckRaises(procedure -> begin pipe.Fit(ds.Data); end, + 'Pipeline must reject DataFrame preprocessors that change RowCount'); +end. diff --git a/TestSuite/_MachineLearning/Regression/010_decisiontree_predictlabels_contract.pas b/TestSuite/_MachineLearning/Regression/010_decisiontree_predictlabels_contract.pas new file mode 100644 index 000000000..1285fb5a3 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/010_decisiontree_predictlabels_contract.pas @@ -0,0 +1,29 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var X := ds.Data.ToMatrix(ds.Features); + + var classes: array of string; + var y := ds.Data.EncodeLabels(ds.Target, classes); + + var model := new DecisionTreeClassifier(maxDepth := 6, seed := 1); + model.Fit(X, y); + + var pred := model.Predict(X); + var labels := model.PredictLabels(X); + var modelClasses := model.GetClassLabels; + + Check(modelClasses.Length = classes.Length, 'class count mismatch'); + Check(pred.Length = X.RowCount, 'Predict length mismatch'); + Check(labels.Length = X.RowCount, 'PredictLabels length mismatch'); + + for var i := 0 to X.RowCount - 1 do + begin + var pi := Round(pred[i]); + Check(Abs(pred[i] - pi) < 1e-12, $'Predict[{i}] is not an original integer label'); + Check((labels[i] >= 0) and (labels[i] < modelClasses.Length), $'PredictLabels[{i}] out of range'); + Check(modelClasses[labels[i]] = pi.ToString, $'Predict[{i}] and PredictLabels[{i}] are inconsistent'); + end; +end. diff --git a/TestSuite/_MachineLearning/Regression/011_decisiontree_getclasslabels_contract.pas b/TestSuite/_MachineLearning/Regression/011_decisiontree_getclasslabels_contract.pas new file mode 100644 index 000000000..25ede7f11 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/011_decisiontree_getclasslabels_contract.pas @@ -0,0 +1,19 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var X := ds.Data.ToMatrix(ds.Features); + + var classes: array of string; + var y := ds.Data.EncodeLabels(ds.Target, classes); + + var model := new DecisionTreeClassifier(maxDepth := 6, seed := 1); + model.Fit(X, y); + + var modelClasses := model.GetClassLabels; + Check(modelClasses.Length = classes.Length, 'class count mismatch'); + + for var i := 0 to classes.Length - 1 do + Check(modelClasses[i] = i.ToString, $'DecisionTree GetClassLabels mismatch at {i}'); +end. diff --git a/TestSuite/_MachineLearning/Regression/012_pipeline_getencodedlabels_unknown_class.pas b/TestSuite/_MachineLearning/Regression/012_pipeline_getencodedlabels_unknown_class.pas new file mode 100644 index 000000000..00e841aea --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/012_pipeline_getencodedlabels_unknown_class.pas @@ -0,0 +1,26 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var trainDf := new DataFrame; + trainDf.AddFloatColumn('X', Arr(0.0, 1.0, 0.0, 1.0)); + trainDf.AddStrColumn('Target', Arr('cat', 'dog', 'cat', 'dog')); + trainDf := trainDf.SetCategorical(['Target']); + + var testDf := new DataFrame; + testDf.AddFloatColumn('X', Arr(0.5, 0.7)); + testDf.AddStrColumn('Target', Arr('cat', 'fox')); + testDf := testDf.SetCategorical(['Target']); + + var pipe := DataPipeline.Build( + TaskKind.tkClassification, + 'Target', + Arr($'X'), + new LogisticRegression + ); + + pipe.Fit(trainDf); + + CheckRaises(procedure -> begin var y := pipe.GetEncodedLabels(testDf); end, + 'GetEncodedLabels must reject unseen target classes'); +end. diff --git a/TestSuite/_MachineLearning/Regression/013_upipeline_rowcount_invariant.pas b/TestSuite/_MachineLearning/Regression/013_upipeline_rowcount_invariant.pas new file mode 100644 index 000000000..9567668f8 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/013_upipeline_rowcount_invariant.pas @@ -0,0 +1,43 @@ +uses MLABC, PreprocessorABC; +uses TestHelpers in '..\TestHelpers.pas'; + +type + BadDropper = class(IPreprocessor) + public + function Fit(df: DataFrame): IPreprocessor; + function Transform(df: DataFrame): DataFrame; + function FitTransform(df: DataFrame): DataFrame; + function Clone: IPreprocessor; + end; + +function BadDropper.Fit(df: DataFrame): IPreprocessor; +begin + Result := Self; +end; + +function BadDropper.Transform(df: DataFrame): DataFrame; +begin + Result := df.Head(df.RowCount - 1); +end; + +function BadDropper.FitTransform(df: DataFrame): DataFrame; +begin + Result := Transform(df); +end; + +function BadDropper.Clone: IPreprocessor; +begin + Result := new BadDropper; +end; + +begin + var ds := Datasets.Iris; + var pipe := UDataPipeline.Build( + ds.Features, + new BadDropper, + new KMeans(3, seed := 1) + ); + + CheckRaises(procedure -> begin var labels := pipe.FitPredict(ds.Data); end, + 'UDataPipeline must reject DataFrame preprocessors that change RowCount'); +end. diff --git a/TestSuite/_MachineLearning/Regression/014_logistic_getclasslabels_contract.pas b/TestSuite/_MachineLearning/Regression/014_logistic_getclasslabels_contract.pas new file mode 100644 index 000000000..79fd8e02e --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/014_logistic_getclasslabels_contract.pas @@ -0,0 +1,19 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var X := ds.Data.ToMatrix(ds.Features); + + var classes: array of string; + var y := ds.Data.EncodeLabels(ds.Target, classes); + + var model := new LogisticRegression; + model.Fit(X, y); + + var modelClasses := model.GetClassLabels; + Check(modelClasses.Length = classes.Length, 'class count mismatch'); + + for var i := 0 to classes.Length - 1 do + Check(modelClasses[i] = i.ToString, $'LogisticRegression GetClassLabels mismatch at {i}'); +end. diff --git a/TestSuite/_MachineLearning/Regression/015_randomforest_getclasslabels_contract.pas b/TestSuite/_MachineLearning/Regression/015_randomforest_getclasslabels_contract.pas new file mode 100644 index 000000000..602167a0d --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/015_randomforest_getclasslabels_contract.pas @@ -0,0 +1,19 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var X := ds.Data.ToMatrix(ds.Features); + + var classes: array of string; + var y := ds.Data.EncodeLabels(ds.Target, classes); + + var model := new RandomForestClassifier(20, maxDepth := 6, seed := 1); + model.Fit(X, y); + + var modelClasses := model.GetClassLabels; + Check(modelClasses.Length = classes.Length, 'class count mismatch'); + + for var i := 0 to classes.Length - 1 do + Check(modelClasses[i] = i.ToString, $'RandomForestClassifier GetClassLabels mismatch at {i}'); +end. diff --git a/TestSuite/_MachineLearning/Regression/016_gradientboosting_getclasslabels_contract.pas b/TestSuite/_MachineLearning/Regression/016_gradientboosting_getclasslabels_contract.pas new file mode 100644 index 000000000..a92b0e180 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/016_gradientboosting_getclasslabels_contract.pas @@ -0,0 +1,19 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var X := ds.Data.ToMatrix(ds.Features); + + var classes: array of string; + var y := ds.Data.EncodeLabels(ds.Target, classes); + + var model := new GradientBoostingClassifier(20, learningRate := 0.1, maxDepth := 3, seed := 1); + model.Fit(X, y); + + var modelClasses := model.GetClassLabels; + Check(modelClasses.Length = classes.Length, 'class count mismatch'); + + for var i := 0 to classes.Length - 1 do + Check(modelClasses[i] = i.ToString, $'GradientBoostingClassifier GetClassLabels mismatch at {i}'); +end. diff --git a/TestSuite/_MachineLearning/Regression/017_knn_getclasslabels_contract.pas b/TestSuite/_MachineLearning/Regression/017_knn_getclasslabels_contract.pas new file mode 100644 index 000000000..e64f17a6b --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/017_knn_getclasslabels_contract.pas @@ -0,0 +1,19 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var X := ds.Data.ToMatrix(ds.Features); + + var classes: array of string; + var y := ds.Data.EncodeLabels(ds.Target, classes); + + var model := new KNNClassifier(5); + model.Fit(X, y); + + var modelClasses := model.GetClassLabels; + Check(modelClasses.Length = classes.Length, 'class count mismatch'); + + for var i := 0 to classes.Length - 1 do + Check(modelClasses[i] = i.ToString, $'KNNClassifier GetClassLabels mismatch at {i}'); +end. diff --git a/TestSuite/_MachineLearning/Regression/018_pipeline_regression_rejects_string_target.pas b/TestSuite/_MachineLearning/Regression/018_pipeline_regression_rejects_string_target.pas new file mode 100644 index 000000000..f4b46427f --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/018_pipeline_regression_rejects_string_target.pas @@ -0,0 +1,19 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddFloatColumn('X', Arr(1.0, 2.0, 3.0)); + df.AddStrColumn('Target', Arr('low', 'mid', 'high')); + df := df.SetCategorical(['Target']); + + var pipe := DataPipeline.Build( + TaskKind.tkRegression, + 'Target', + Arr($'X'), + new LinearRegression + ); + + CheckRaises(procedure -> begin pipe.Fit(df); end, + 'Regression pipeline must reject non-numeric target'); +end. diff --git a/TestSuite/_MachineLearning/Regression/019_pipeline_classification_requires_categorical_target.pas b/TestSuite/_MachineLearning/Regression/019_pipeline_classification_requires_categorical_target.pas new file mode 100644 index 000000000..2f72ccd48 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/019_pipeline_classification_requires_categorical_target.pas @@ -0,0 +1,18 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddFloatColumn('X', Arr(1.0, 2.0, 3.0, 4.0)); + df.AddStrColumn('Target', Arr('cat', 'dog', 'cat', 'dog')); + + var pipe := DataPipeline.Build( + TaskKind.tkClassification, + 'Target', + Arr($'X'), + new LogisticRegression + ); + + CheckRaises(procedure -> begin pipe.Fit(df); end, + 'Classification pipeline must require categorical target'); +end. diff --git a/TestSuite/_MachineLearning/Regression/020_logistic_predictproba_shape.pas b/TestSuite/_MachineLearning/Regression/020_logistic_predictproba_shape.pas new file mode 100644 index 000000000..5d2050068 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/020_logistic_predictproba_shape.pas @@ -0,0 +1,19 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var X := ds.Data.ToMatrix(ds.Features); + + var classes: array of string; + var y := ds.Data.EncodeLabels(ds.Target, classes); + + var model := new LogisticRegression; + model.Fit(X, y); + + var proba := model.PredictProba(X); + + Check(proba.RowCount = X.RowCount, 'Probability row count mismatch'); + Check(proba.ColCount = classes.Length, 'Probability class count mismatch'); + CheckProbabilityRowsSumToOne(proba); +end. diff --git a/TestSuite/_MachineLearning/Regression/021_randomforest_predictproba_shape.pas b/TestSuite/_MachineLearning/Regression/021_randomforest_predictproba_shape.pas new file mode 100644 index 000000000..22554d7c4 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/021_randomforest_predictproba_shape.pas @@ -0,0 +1,19 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var X := ds.Data.ToMatrix(ds.Features); + + var classes: array of string; + var y := ds.Data.EncodeLabels(ds.Target, classes); + + var model := new RandomForestClassifier(20, maxDepth := 6, seed := 1); + model.Fit(X, y); + + var proba := model.PredictProba(X); + + Check(proba.RowCount = X.RowCount, 'Probability row count mismatch'); + Check(proba.ColCount = classes.Length, 'Probability class count mismatch'); + CheckProbabilityRowsSumToOne(proba); +end. diff --git a/TestSuite/_MachineLearning/Regression/022_gradientboosting_predictproba_shape.pas b/TestSuite/_MachineLearning/Regression/022_gradientboosting_predictproba_shape.pas new file mode 100644 index 000000000..f7bec5daa --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/022_gradientboosting_predictproba_shape.pas @@ -0,0 +1,19 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var X := ds.Data.ToMatrix(ds.Features); + + var classes: array of string; + var y := ds.Data.EncodeLabels(ds.Target, classes); + + var model := new GradientBoostingClassifier(20, learningRate := 0.1, maxDepth := 3, seed := 1); + model.Fit(X, y); + + var proba := model.PredictProba(X); + + Check(proba.RowCount = X.RowCount, 'Probability row count mismatch'); + Check(proba.ColCount = classes.Length, 'Probability class count mismatch'); + CheckProbabilityRowsSumToOne(proba); +end. diff --git a/TestSuite/_MachineLearning/Regression/023_pipeline_predictproba_shape.pas b/TestSuite/_MachineLearning/Regression/023_pipeline_predictproba_shape.pas new file mode 100644 index 000000000..df92fb040 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/023_pipeline_predictproba_shape.pas @@ -0,0 +1,26 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var df := ds.Data; + + var pipe := + DataPipeline.Build( + TaskKind.tkClassification, + ds.Target, + ds.Features, + new StandardScaler, + new LogisticRegression + ); + + var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 3); + pipe.Fit(trainDf); + + var classes := pipe.GetClassLabels; + var proba := pipe.PredictProba(testDf); + + Check(proba.RowCount = testDf.RowCount, 'Probability row count mismatch'); + Check(proba.ColCount = classes.Length, 'Probability class count mismatch'); + CheckProbabilityRowsSumToOne(proba); +end. diff --git a/TestSuite/_MachineLearning/Regression/024_pipeline_target_in_features_rejected.pas b/TestSuite/_MachineLearning/Regression/024_pipeline_target_in_features_rejected.pas new file mode 100644 index 000000000..10d24472e --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/024_pipeline_target_in_features_rejected.pas @@ -0,0 +1,14 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + CheckRaises(procedure -> begin + var pipe := DataPipeline.Build( + TaskKind.tkClassification, + 'Target', + Arr($'X', $'Target'), + new LogisticRegression + ); + end, + 'DataPipeline.Build must reject target inside features'); +end. diff --git a/TestSuite/_MachineLearning/Regression/025_upipeline_duplicate_features_rejected.pas b/TestSuite/_MachineLearning/Regression/025_upipeline_duplicate_features_rejected.pas new file mode 100644 index 000000000..ba605165d --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/025_upipeline_duplicate_features_rejected.pas @@ -0,0 +1,12 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + CheckRaises(procedure -> begin + var pipe := UDataPipeline.Build( + Arr($'X', $'X'), + new KMeans(3, seed := 1) + ); + end, + 'UDataPipeline.Build must reject duplicate features'); +end. diff --git a/TestSuite/_MachineLearning/Regression/026_pipeline_predictlabels_returns_original_strings.pas b/TestSuite/_MachineLearning/Regression/026_pipeline_predictlabels_returns_original_strings.pas new file mode 100644 index 000000000..b19e7dbf7 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/026_pipeline_predictlabels_returns_original_strings.pas @@ -0,0 +1,27 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var df := ds.Data; + var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 3); + + var pipe := + DataPipeline.Build( + TaskKind.tkClassification, + ds.Target, + ds.Features, + new StandardScaler, + new LogisticRegression + ); + + pipe.Fit(trainDf); + + var labels := pipe.PredictLabels(testDf); + var classes := pipe.GetClassLabels; + + Check(labels.Length = testDf.RowCount, 'PredictLabels length mismatch'); + + for var i := 0 to labels.Length - 1 do + Check(labels[i] in classes, $'PredictLabels[{i}] must be one of pipeline class labels'); +end. diff --git a/TestSuite/_MachineLearning/Regression/027_join_float_key_rejected.pas b/TestSuite/_MachineLearning/Regression/027_join_float_key_rejected.pas new file mode 100644 index 000000000..ffd32fc8a --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/027_join_float_key_rejected.pas @@ -0,0 +1,15 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var left := new DataFrame; + left.AddFloatColumn('id', Arr(1.0, 2.0)); + left.AddStrColumn('name', Arr('A', 'B')); + + var right := new DataFrame; + right.AddFloatColumn('id', Arr(1.0, 2.0)); + right.AddFloatColumn('score', Arr(10.0, 20.0)); + + CheckRaises(procedure -> begin var res := left.Join(right, 'id'); end, + 'Join on float key must be rejected'); +end. diff --git a/TestSuite/_MachineLearning/Regression/028_pipeline_target_bound_step_blocked.pas b/TestSuite/_MachineLearning/Regression/028_pipeline_target_bound_step_blocked.pas new file mode 100644 index 000000000..592494065 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/028_pipeline_target_bound_step_blocked.pas @@ -0,0 +1,53 @@ +uses MLABC, PreprocessorABC, MLCoreABC; +uses TestHelpers in '..\TestHelpers.pas'; + +type + BadTargetStep = class(IPreprocessor, IColumnBoundStep) + private + fColumn: string; + public + constructor Create(column: string); + function Fit(df: DataFrame): IPreprocessor; + function Transform(df: DataFrame): DataFrame; + function FitTransform(df: DataFrame): DataFrame; + function Clone: IPreprocessor; + property ColumnName: string read fColumn; + end; + +constructor BadTargetStep.Create(column: string); +begin + fColumn := column; +end; + +function BadTargetStep.Fit(df: DataFrame): IPreprocessor; +begin + Result := Self; +end; + +function BadTargetStep.Transform(df: DataFrame): DataFrame; +begin + Result := df; +end; + +function BadTargetStep.FitTransform(df: DataFrame): DataFrame; +begin + Result := df; +end; + +function BadTargetStep.Clone: IPreprocessor; +begin + Result := new BadTargetStep(fColumn); +end; + +begin + CheckRaises(procedure -> begin + var pipe := DataPipeline.Build( + TaskKind.tkClassification, + 'Target', + Arr($'X'), + new BadTargetStep('Target'), + new LogisticRegression + ); + end, + 'DataPipeline must reject a bound preprocessor that targets the target column'); +end. diff --git a/TestSuite/_MachineLearning/Regression/029_pipeline_target_columns_step_blocked.pas b/TestSuite/_MachineLearning/Regression/029_pipeline_target_columns_step_blocked.pas new file mode 100644 index 000000000..5f027e78d --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/029_pipeline_target_columns_step_blocked.pas @@ -0,0 +1,53 @@ +uses MLABC, PreprocessorABC, MLCoreABC; +uses TestHelpers in '..\TestHelpers.pas'; + +type + BadTargetColumnsStep = class(IPreprocessor, IColumnsBoundStep) + private + fColumns: array of string; + public + constructor Create(columns: array of string); + function Fit(df: DataFrame): IPreprocessor; + function Transform(df: DataFrame): DataFrame; + function FitTransform(df: DataFrame): DataFrame; + function Clone: IPreprocessor; + property Columns: array of string read fColumns; + end; + +constructor BadTargetColumnsStep.Create(columns: array of string); +begin + fColumns := Copy(columns); +end; + +function BadTargetColumnsStep.Fit(df: DataFrame): IPreprocessor; +begin + Result := Self; +end; + +function BadTargetColumnsStep.Transform(df: DataFrame): DataFrame; +begin + Result := df; +end; + +function BadTargetColumnsStep.FitTransform(df: DataFrame): DataFrame; +begin + Result := df; +end; + +function BadTargetColumnsStep.Clone: IPreprocessor; +begin + Result := new BadTargetColumnsStep(fColumns); +end; + +begin + CheckRaises(procedure -> begin + var pipe := DataPipeline.Build( + TaskKind.tkClassification, + 'Target', + Arr($'X'), + new BadTargetColumnsStep(Arr($'X', $'Target')), + new LogisticRegression + ); + end, + 'DataPipeline must reject a multi-column bound preprocessor that includes the target column'); +end. diff --git a/TestSuite/_MachineLearning/Regression/030_pipeline_int_categorical_target_contract.pas b/TestSuite/_MachineLearning/Regression/030_pipeline_int_categorical_target_contract.pas new file mode 100644 index 000000000..1cc1d3851 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/030_pipeline_int_categorical_target_contract.pas @@ -0,0 +1,41 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddFloatColumn('X', Arr(0.0, 1.0, 0.0, 1.0)); + df.AddIntColumn('Target', Arr(10, 20, 10, 20)); + df := df.SetCategorical(['Target']); + + var pipe := DataPipeline.Build( + TaskKind.tkClassification, + 'Target', + Arr($'X'), + new LogisticRegression + ); + + pipe.Fit(df); + + var pred := pipe.Predict(df); + var predLabels := pipe.PredictLabels(df); + var enc := pipe.GetEncodedLabels(df); + var classes := pipe.GetClassLabels; + + Check(classes.Length = 2, 'Class count mismatch'); + Check((classes[0] = '10') or (classes[0] = '20'), 'Unexpected first class label'); + Check((classes[1] = '10') or (classes[1] = '20'), 'Unexpected second class label'); + Check(classes[0] <> classes[1], 'Class labels must be distinct'); + + Check(pred.Length = df.RowCount, 'Predict length mismatch'); + Check(predLabels.Length = df.RowCount, 'PredictLabels length mismatch'); + Check(enc.Length = df.RowCount, 'GetEncodedLabels length mismatch'); + + for var i := 0 to df.RowCount - 1 do + begin + var pi := Round(pred[i]); + var ei := Round(enc[i]); + Check((pi >= 0) and (pi < classes.Length), $'Predict[{i}] out of range'); + Check((ei >= 0) and (ei < classes.Length), $'GetEncodedLabels[{i}] out of range'); + Check(predLabels[i] = classes[pi], $'PredictLabels[{i}] mismatch'); + end; +end. diff --git a/TestSuite/_MachineLearning/Regression/031_logistic_sparse_raw_labels_contract.pas b/TestSuite/_MachineLearning/Regression/031_logistic_sparse_raw_labels_contract.pas new file mode 100644 index 000000000..a3e5d31cb --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/031_logistic_sparse_raw_labels_contract.pas @@ -0,0 +1,38 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var X := new Matrix(6, 1); + X[0,0] := 0.0; X[1,0] := 0.1; + X[2,0] := 1.0; X[3,0] := 1.1; + X[4,0] := 2.0; X[5,0] := 2.1; + + var y := new Vector(Arr(10.0, 10.0, 20.0, 20.0, 30.0, 30.0)); + + var model := new LogisticRegression; + model.Fit(X, y); + + var pred := model.Predict(X); + var labels := model.PredictLabels(X); + var classes := model.GetClassLabels; + var proba := model.PredictProba(X); + + Check(classes.Length = 3, 'Class count mismatch'); + Check(classes[0] = '10', 'First class label mismatch'); + Check(classes[1] = '20', 'Second class label mismatch'); + Check(classes[2] = '30', 'Third class label mismatch'); + + Check(pred.Length = X.RowCount, 'Predict length mismatch'); + Check(labels.Length = X.RowCount, 'PredictLabels length mismatch'); + Check(proba.RowCount = X.RowCount, 'PredictProba row count mismatch'); + Check(proba.ColCount = classes.Length, 'PredictProba class count mismatch'); + CheckProbabilityRowsSumToOne(proba); + + for var i := 0 to X.RowCount - 1 do + begin + var pi := Round(pred[i]); + Check((labels[i] >= 0) and (labels[i] < classes.Length), $'PredictLabels[{i}] out of range'); + Check((pi = 10) or (pi = 20) or (pi = 30), $'Predict[{i}] must return original sparse label'); + Check(classes[labels[i]] = pi.ToString, $'Predict and PredictLabels mismatch at {i}'); + end; +end. diff --git a/TestSuite/_MachineLearning/Regression/032_randomforest_sparse_raw_labels_contract.pas b/TestSuite/_MachineLearning/Regression/032_randomforest_sparse_raw_labels_contract.pas new file mode 100644 index 000000000..425fe8ed5 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/032_randomforest_sparse_raw_labels_contract.pas @@ -0,0 +1,33 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var X := new Matrix(6, 1); + X[0,0] := 0.0; X[1,0] := 0.1; + X[2,0] := 1.0; X[3,0] := 1.1; + X[4,0] := 2.0; X[5,0] := 2.1; + + var y := new Vector(Arr(10.0, 10.0, 20.0, 20.0, 30.0, 30.0)); + + var model := new RandomForestClassifier(20, maxDepth := 6, seed := 1); + model.Fit(X, y); + + var pred := model.Predict(X); + var labels := model.PredictLabels(X); + var classes := model.GetClassLabels; + var proba := model.PredictProba(X); + + Check(classes.Length = 3, 'Class count mismatch'); + Check(classes[0] = '10', 'First class label mismatch'); + Check(classes[1] = '20', 'Second class label mismatch'); + Check(classes[2] = '30', 'Third class label mismatch'); + CheckProbabilityRowsSumToOne(proba); + + for var i := 0 to X.RowCount - 1 do + begin + var pi := Round(pred[i]); + Check((labels[i] >= 0) and (labels[i] < classes.Length), $'PredictLabels[{i}] out of range'); + Check((pi = 10) or (pi = 20) or (pi = 30), $'Predict[{i}] must return original sparse label'); + Check(classes[labels[i]] = pi.ToString, $'Predict and PredictLabels mismatch at {i}'); + end; +end. diff --git a/TestSuite/_MachineLearning/Regression/033_gradientboosting_sparse_raw_labels_contract.pas b/TestSuite/_MachineLearning/Regression/033_gradientboosting_sparse_raw_labels_contract.pas new file mode 100644 index 000000000..6f38b4c11 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/033_gradientboosting_sparse_raw_labels_contract.pas @@ -0,0 +1,33 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var X := new Matrix(6, 1); + X[0,0] := 0.0; X[1,0] := 0.1; + X[2,0] := 1.0; X[3,0] := 1.1; + X[4,0] := 2.0; X[5,0] := 2.1; + + var y := new Vector(Arr(10.0, 10.0, 20.0, 20.0, 30.0, 30.0)); + + var model := new GradientBoostingClassifier(20, learningRate := 0.1, maxDepth := 3, seed := 1); + model.Fit(X, y); + + var pred := model.Predict(X); + var labels := model.PredictLabels(X); + var classes := model.GetClassLabels; + var proba := model.PredictProba(X); + + Check(classes.Length = 3, 'Class count mismatch'); + Check(classes[0] = '10', 'First class label mismatch'); + Check(classes[1] = '20', 'Second class label mismatch'); + Check(classes[2] = '30', 'Third class label mismatch'); + CheckProbabilityRowsSumToOne(proba); + + for var i := 0 to X.RowCount - 1 do + begin + var pi := Round(pred[i]); + Check((labels[i] >= 0) and (labels[i] < classes.Length), $'PredictLabels[{i}] out of range'); + Check((pi = 10) or (pi = 20) or (pi = 30), $'Predict[{i}] must return original sparse label'); + Check(classes[labels[i]] = pi.ToString, $'Predict and PredictLabels mismatch at {i}'); + end; +end. diff --git a/TestSuite/_MachineLearning/Regression/034_decisiontree_sparse_raw_labels_contract.pas b/TestSuite/_MachineLearning/Regression/034_decisiontree_sparse_raw_labels_contract.pas new file mode 100644 index 000000000..90bcaf834 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/034_decisiontree_sparse_raw_labels_contract.pas @@ -0,0 +1,31 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var X := new Matrix(6, 1); + X[0,0] := 0.0; X[1,0] := 0.1; + X[2,0] := 1.0; X[3,0] := 1.1; + X[4,0] := 2.0; X[5,0] := 2.1; + + var y := new Vector(Arr(10.0, 10.0, 20.0, 20.0, 30.0, 30.0)); + + var model := new DecisionTreeClassifier(maxDepth := 6, seed := 1); + model.Fit(X, y); + + var pred := model.Predict(X); + var labels := model.PredictLabels(X); + var classes := model.GetClassLabels; + + Check(classes.Length = 3, 'Class count mismatch'); + Check(classes[0] = '10', 'First class label mismatch'); + Check(classes[1] = '20', 'Second class label mismatch'); + Check(classes[2] = '30', 'Third class label mismatch'); + + for var i := 0 to X.RowCount - 1 do + begin + var pi := Round(pred[i]); + Check((labels[i] >= 0) and (labels[i] < classes.Length), $'PredictLabels[{i}] out of range'); + Check((pi = 10) or (pi = 20) or (pi = 30), $'Predict[{i}] must return original sparse label'); + Check(classes[labels[i]] = pi.ToString, $'Predict and PredictLabels mismatch at {i}'); + end; +end. diff --git a/TestSuite/_MachineLearning/Regression/035_knn_sparse_raw_labels_contract.pas b/TestSuite/_MachineLearning/Regression/035_knn_sparse_raw_labels_contract.pas new file mode 100644 index 000000000..184f119ec --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/035_knn_sparse_raw_labels_contract.pas @@ -0,0 +1,33 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var X := new Matrix(6, 1); + X[0,0] := 0.0; X[1,0] := 0.1; + X[2,0] := 1.0; X[3,0] := 1.1; + X[4,0] := 2.0; X[5,0] := 2.1; + + var y := new Vector(Arr(10.0, 10.0, 20.0, 20.0, 30.0, 30.0)); + + var model := new KNNClassifier(3); + model.Fit(X, y); + + var pred := model.Predict(X); + var labels := model.PredictLabels(X); + var classes := model.GetClassLabels; + var proba := model.PredictProba(X); + + Check(classes.Length = 3, 'Class count mismatch'); + Check(classes[0] = '10', 'First class label mismatch'); + Check(classes[1] = '20', 'Second class label mismatch'); + Check(classes[2] = '30', 'Third class label mismatch'); + CheckProbabilityRowsSumToOne(proba); + + for var i := 0 to X.RowCount - 1 do + begin + var pi := Round(pred[i]); + Check((labels[i] >= 0) and (labels[i] < classes.Length), $'PredictLabels[{i}] out of range'); + Check((pi = 10) or (pi = 20) or (pi = 30), $'Predict[{i}] must return original sparse label'); + Check(classes[labels[i]] = pi.ToString, $'Predict and PredictLabels mismatch at {i}'); + end; +end. diff --git a/TestSuite/_MachineLearning/Regression/036_pipeline_predict_dense_encoded_labels.pas b/TestSuite/_MachineLearning/Regression/036_pipeline_predict_dense_encoded_labels.pas new file mode 100644 index 000000000..1e249fc5e --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/036_pipeline_predict_dense_encoded_labels.pas @@ -0,0 +1,32 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var df := ds.Data; + var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 3); + + var pipe := + DataPipeline.Build( + TaskKind.tkClassification, + ds.Target, + ds.Features, + new StandardScaler, + new LogisticRegression + ); + + pipe.Fit(trainDf); + + var pred := pipe.Predict(testDf); + var classes := pipe.GetClassLabels; + + Check(pred.Length = testDf.RowCount, 'Predict length mismatch'); + Check(classes.Length > 0, 'Class labels must not be empty'); + + for var i := 0 to pred.Length - 1 do + begin + var pi := Round(pred[i]); + Check(Abs(pred[i] - pi) < 1e-12, $'Predict[{i}] must be a dense encoded integer'); + Check((pi >= 0) and (pi < classes.Length), $'Predict[{i}] out of dense class range'); + end; +end. diff --git a/TestSuite/_MachineLearning/Regression/037_pipeline_predict_contract_matches_documented_semantics.pas b/TestSuite/_MachineLearning/Regression/037_pipeline_predict_contract_matches_documented_semantics.pas new file mode 100644 index 000000000..7deba4649 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/037_pipeline_predict_contract_matches_documented_semantics.pas @@ -0,0 +1,35 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var ds := Datasets.Iris; + var df := ds.Data; + var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 3); + + var pipe := + DataPipeline.Build( + TaskKind.tkClassification, + ds.Target, + ds.Features, + new StandardScaler, + new LogisticRegression + ); + + pipe.Fit(trainDf); + + var pred := pipe.Predict(testDf); + var predLabels := pipe.PredictLabels(testDf); + var classes := pipe.GetClassLabels; + + Check(pred.Length = testDf.RowCount, 'Predict length mismatch'); + Check(predLabels.Length = testDf.RowCount, 'PredictLabels length mismatch'); + Check(classes.Length > 0, 'Class labels must not be empty'); + + for var i := 0 to pred.Length - 1 do + begin + var pi := Round(pred[i]); + Check(Abs(pred[i] - pi) < 1e-12, $'Predict[{i}] must be an internal class index'); + Check((pi >= 0) and (pi < classes.Length), $'Predict[{i}] out of range'); + Check(predLabels[i] = classes[pi], $'PredictLabels[{i}] must decode Predict[{i}]'); + end; +end. diff --git a/TestSuite/_MachineLearning/Regression/038_standalone_classifier_predict_contract_is_explicit.pas b/TestSuite/_MachineLearning/Regression/038_standalone_classifier_predict_contract_is_explicit.pas new file mode 100644 index 000000000..2f7e722e2 --- /dev/null +++ b/TestSuite/_MachineLearning/Regression/038_standalone_classifier_predict_contract_is_explicit.pas @@ -0,0 +1,28 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var X := new Matrix(6, 1); + X[0,0] := 0.0; X[1,0] := 0.1; + X[2,0] := 1.0; X[3,0] := 1.1; + X[4,0] := 2.0; X[5,0] := 2.1; + + var y := new Vector(Arr(10.0, 10.0, 20.0, 20.0, 30.0, 30.0)); + + var model := new LogisticRegression; + model.Fit(X, y); + + var pred := model.Predict(X); + var labels := model.PredictLabels(X); + var classes := model.GetClassLabels; + + Check(classes.Length = 3, 'Class count mismatch'); + + for var i := 0 to X.RowCount - 1 do + begin + var pi := Round(pred[i]); + Check((pi = 10) or (pi = 20) or (pi = 30), $'Predict[{i}] must return original class label'); + Check((labels[i] >= 0) and (labels[i] < classes.Length), $'PredictLabels[{i}] out of range'); + Check(classes[labels[i]] = pi.ToString, $'PredictLabels[{i}] must point to Predict[{i}]'); + end; +end. diff --git a/TestSuite/_MachineLearning/TestHelpers.pas b/TestSuite/_MachineLearning/TestHelpers.pas new file mode 100644 index 000000000..33da14306 --- /dev/null +++ b/TestSuite/_MachineLearning/TestHelpers.pas @@ -0,0 +1,63 @@ +unit TestHelpers; + +interface + +uses MLABC; + +procedure Check(cond: boolean; msg: string); +procedure CheckRaises(action: procedure; msg: string); +procedure CheckSchemaMatchesColumns(df: DataFrame); +procedure CheckSchemaMatchesColumns(df: DataFrame; expectedCats: array of boolean); +procedure CheckProbabilityRowsSumToOne(m: Matrix; eps: real := 1e-9); + +implementation + +procedure Check(cond: boolean; msg: string); +begin + if not cond then + raise new Exception(msg); +end; + +procedure CheckRaises(action: procedure; msg: string); +begin + var raised := false; + try + action(); + except + on e: Exception do + raised := true; + end; + Check(raised, msg); +end; + +procedure CheckSchemaMatchesColumns(df: DataFrame); +begin + Check(df.Schema.ColumnCount = df.ColumnCount, 'Schema/column count mismatch'); + for var i := 0 to df.ColumnCount - 1 do + begin + Check(df.Schema.NameAt(i) = df.GetColumn(i).Info.Name, $'Name mismatch at {i}'); + Check(df.Schema.ColumnTypeAt(i) = df.GetColumn(i).Info.ColType, $'Type mismatch at {i}'); + Check(df.GetColumn(i).RowCount = df.RowCount, $'RowCount mismatch at column {i}'); + end; +end; + +procedure CheckSchemaMatchesColumns(df: DataFrame; expectedCats: array of boolean); +begin + CheckSchemaMatchesColumns(df); + Check(expectedCats.Length = df.ColumnCount, 'Expected categorical flags length mismatch'); + for var i := 0 to df.ColumnCount - 1 do + Check(df.Schema.IsCategoricalAt(i) = expectedCats[i], $'Categorical mismatch at {i}'); +end; + +procedure CheckProbabilityRowsSumToOne(m: Matrix; eps: real); +begin + for var i := 0 to m.RowCount - 1 do + begin + var s := 0.0; + for var j := 0 to m.ColCount - 1 do + s += m[i, j]; + Check(Abs(s - 1.0) < eps, $'Probability row {i} must sum to 1'); + end; +end; + +end. diff --git a/TestSuite/_MachineLearning/clean_ml_tests.cmd b/TestSuite/_MachineLearning/clean_ml_tests.cmd new file mode 100644 index 000000000..ea9b9d6a6 --- /dev/null +++ b/TestSuite/_MachineLearning/clean_ml_tests.cmd @@ -0,0 +1,6 @@ +@echo off +chcp 65001 >nul +for /r "%~dp0" %%F in (*.exe) do del /q "%%F" +for /r "%~dp0" %%F in (*.pdb) do del /q "%%F" +echo Done. +pause diff --git a/TestSuite/_MachineLearning/run_ml_tests.cmd b/TestSuite/_MachineLearning/run_ml_tests.cmd new file mode 100644 index 000000000..89248d468 --- /dev/null +++ b/TestSuite/_MachineLearning/run_ml_tests.cmd @@ -0,0 +1,4 @@ +@echo off +chcp 65001 >nul +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0run_ml_tests.ps1" -Fast +pause diff --git a/TestSuite/_MachineLearning/run_ml_tests.ps1 b/TestSuite/_MachineLearning/run_ml_tests.ps1 new file mode 100644 index 000000000..bf5259c48 --- /dev/null +++ b/TestSuite/_MachineLearning/run_ml_tests.ps1 @@ -0,0 +1,121 @@ +param( + [string]$Root = 'D:\PABC_GIT\TestSuite\_MachineLearning', + [switch]$Fast +) + +[Console]::InputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +$OutputEncoding = [System.Text.Encoding]::UTF8 + +$compiler = 'D:\PABC_GIT\bin\pabcnetcclear.exe' +$libRoot = 'D:\PABC_GIT\bin\Lib' +$tests = Get-ChildItem -Path $Root -Recurse -Filter *.pas | Where-Object { $_.Name -ne 'TestHelpers.pas' } | Sort-Object FullName +$libStamp = (Get-ChildItem -Path $libRoot -Recurse -Filter *.pas | Measure-Object LastWriteTime -Maximum).Maximum +$testHelpers = Join-Path $Root 'TestHelpers.pas' +$helperStamp = if (Test-Path $testHelpers) { (Get-Item $testHelpers).LastWriteTime } else { Get-Date '2000-01-01' } + +if ($tests.Count -eq 0) { + Write-Host 'No tests found.' + exit 0 +} + +$failed = @() +$passedCount = 0 +$progressOnLine = 0 + +Write-Host "Found tests: $($tests.Count)" +if ($Fast) { + Write-Host 'Mode: FAST' +} +Write-Host '' + +foreach ($test in $tests) { + $exe = [System.IO.Path]::ChangeExtension($test.FullName, '.exe') + $pdb = [System.IO.Path]::ChangeExtension($test.FullName, '.pdb') + + $needCompile = $true + if ($Fast -and (Test-Path $exe)) { + $exeStamp = (Get-Item $exe).LastWriteTime + if (($exeStamp -ge $test.LastWriteTime) -and ($exeStamp -ge $libStamp) -and ($exeStamp -ge $helperStamp)) { + $needCompile = $false + } + } + + if ($needCompile) { + $compileOut = & $compiler "/SearchDir:$Root" $test.FullName 2>&1 + if ($LASTEXITCODE -ne 0) { + if ($progressOnLine -gt 0) { + Write-Host '' + $progressOnLine = 0 + } + Write-Host "=== $($test.FullName) ===" + if ($compileOut) { $compileOut | ForEach-Object { Write-Host $_ } } + Write-Host 'COMPILE FAIL' + Write-Host '' + $failed += "$($test.FullName) [compile]" + continue + } + } + + if (-not (Test-Path $exe)) { + if ($progressOnLine -gt 0) { + Write-Host '' + $progressOnLine = 0 + } + Write-Host "=== $($test.FullName) ===" + Write-Host 'EXE NOT FOUND' + Write-Host '' + $failed += "$($test.FullName) [no exe]" + continue + } + + $runOut = & $exe 2>&1 + $runCode = $LASTEXITCODE + + if (-not $Fast) { + if (Test-Path $exe) { Remove-Item -LiteralPath $exe -Force } + if (Test-Path $pdb) { Remove-Item -LiteralPath $pdb -Force } + } + + if ($runCode -ne 0) { + if ($progressOnLine -gt 0) { + Write-Host '' + $progressOnLine = 0 + } + Write-Host "=== $($test.FullName) ===" + if ($runOut) { $runOut | ForEach-Object { Write-Host $_ } } + Write-Host 'RUN FAIL' + Write-Host '' + $failed += "$($test.FullName) [run]" + continue + } + + $passedCount += 1 + Write-Host -NoNewline '.' + $progressOnLine += 1 + if ($progressOnLine -ge 10) { + Write-Host '' + $progressOnLine = 0 + } +} + +if ($progressOnLine -gt 0) { + Write-Host '' +} + +Write-Host '' +Write-Host 'SUMMARY' +Write-Host " Total : $($tests.Count)" +Write-Host " Passed: $passedCount" +Write-Host " Failed: $($failed.Count)" + +if ($failed.Count -gt 0) { + Write-Host '' + Write-Host 'FAILED TESTS:' + $failed | ForEach-Object { Write-Host $_ } + exit 1 +} + +Write-Host '' +Write-Host 'ALL TESTS PASSED' +exit 0 diff --git a/bin/Lib/DataAdapters.pas b/bin/Lib/DataAdapters.pas index 36d5e3520..3777fa7ed 100644 --- a/bin/Lib/DataAdapters.pas +++ b/bin/Lib/DataAdapters.pas @@ -71,10 +71,10 @@ begin end; end; -/// Кодирует строковые метки классов в целочисленные индексы. -/// Каждому уникальному значению присваивается номер 0,1,2,... +/// Кодирует строковые метки классов в целочисленные индексы (0..K-1). /// Порядок кодирования соответствует порядку первого появления меток. -/// Используется при обучении моделей и визуализации. +/// Используйте только при ручной подготовке данных (без DataPipeline). +/// Вызывать только ДО разбиения на train/test, чтобы избежать рассинхронизации меток. function EncodeLabels(Self: DataFrame; target: string): array of integer; extensionmethod; begin if Self = nil then diff --git a/bin/Lib/DataFrameABC.pas b/bin/Lib/DataFrameABC.pas index 63e3fc97d..6819e3b64 100644 --- a/bin/Lib/DataFrameABC.pas +++ b/bin/Lib/DataFrameABC.pas @@ -48,6 +48,9 @@ type fschema: DataFrameSchema; procedure RebuildSchema; + procedure CommitAddedColumn(c: Column); + procedure ValidateColumnsAgainstSchema; + procedure ValidateColumnsAgainstSchema(candidate: DataFrameSchema); // Single key методы function JoinInnerSingleKey(other: DataFrame; key: string): DataFrame; @@ -78,7 +81,6 @@ type procedure AssertSchemaConsistent; // Проверка инвариантов в Debug constructor Create(cols: List; schema: DataFrameSchema); - constructor Create(cols: List); function BuildJoinSchema(right: DataFrame; leftKeys, rightKeys: array of integer; rightPrefix: string): DataFrameSchema; @@ -87,14 +89,15 @@ type function CreateEmptyBySchema(schema: DataFrameSchema): DataFrame; function GetColumn(name: string): Column; + function GetSchema: DataFrameSchema; - function CloneWithCopiedColumns: DataFrame; + function CloneWithSharedColumns: DataFrame; public /// Создает пустой DataFrame constructor Create; - /// Схема DataFrame: имена, типы и признаки категориальности - property Schema: DataFrameSchema read fschema; + /// Возвращает копию схемы DataFrame + property Schema: DataFrameSchema read GetSchema; procedure SetSchema(schema: DataFrameSchema); @@ -102,10 +105,16 @@ type function IsCategorical(name: string): boolean; + /// Возвращает внутренний столбец без копирования. + /// Не изменяйте его, иначе DataFrame будет повреждён. property Item[name: string]: Column read GetColumn; default; + /// Возвращает внутренние столбцы без копирования. + /// Не изменяйте их, иначе DataFrame будет повреждён. function GetColumns: sequence of Column; + /// Возвращает внутренний столбец без копирования. + /// Не изменяйте его, иначе DataFrame будет повреждён. function GetColumn(i: integer): Column; /// Добавляет в DataFrame столбец-синоним, @@ -145,29 +154,57 @@ type /// Возвращает кортеж (trainDataFrame, testDataFrame). function TrainTestSplit(testRatio: real := 0.2; shuffle: boolean := true; seed: integer := -1): (DataFrame, DataFrame); - /// Добавляет столбец целых чисел + /// Разбивает таблицу на обучающую и тестовую выборки с сохранением + /// распределения целевой переменной (стратификация). + /// + /// Строки группируются по значениям столбца target, после чего внутри + /// каждой группы случайным образом перемешиваются и делятся на train и test + /// в заданной пропорции. + /// + /// testRatio = 0.2 означает, что примерно 20% строк из каждой группы + /// попадут в тестовую выборку. + /// + /// Используется только для задач классификации (категориальный target). + /// + /// Если seed >= 0, разбиение будет детерминированным. + /// Если seed = -1, используется случайная инициализация генератора. + /// + /// Возвращает кортеж (trainDataFrame, testDataFrame). + function StratifiedTrainTestSplit(target: string; testRatio: real := 0.2; seed: integer := -1): (DataFrame, DataFrame); + + /// Добавляет столбец целых чисел. + /// Переданные массивы сохраняются как есть. + /// Не изменяйте их после передачи в DataFrame. procedure AddIntColumn(name: string; data: array of integer; valid: array of boolean := nil); - /// Добавляет столбец вещественных чисел + /// Добавляет столбец вещественных чисел. + /// Переданные массивы сохраняются как есть. + /// Не изменяйте их после передачи в DataFrame. procedure AddFloatColumn(name: string; data: array of real; valid: array of boolean := nil); - /// Добавляет строковый столбец + /// Добавляет строковый столбец. + /// Переданные массивы сохраняются как есть. + /// Не изменяйте их после передачи в DataFrame. procedure AddStrColumn(name: string; data: array of string; valid: array of boolean := nil); - /// Добавляет строковый столбец + /// Добавляет строковый столбец. + /// Переданные массивы сохраняются как есть. + /// Не изменяйте их после передачи в DataFrame. procedure AddStrColumn(name: string; data: array of char; valid: array of boolean := nil); - /// Добавляет столбец логических значений + /// Добавляет столбец логических значений. + /// Переданные массивы сохраняются как есть. + /// Не изменяйте их после передачи в DataFrame. procedure AddBoolColumn(name: string; data: array of boolean; valid: array of boolean := nil); - /// Возвращает данные целочисленного столбца по имени - /// Не изменяйте их, иначе DataFrame будет повреждён + /// Возвращает внутренний буфер целочисленного столбца без копирования. + /// Не изменяйте его, иначе DataFrame будет повреждён. function GetIntColumn(name: string): array of integer; - /// Возвращает данные вещественного столбца по имени - /// Не изменяйте их, иначе DataFrame будет повреждён + /// Возвращает внутренний буфер вещественного столбца без копирования. + /// Не изменяйте его, иначе DataFrame будет повреждён. function GetFloatColumn(name: string): array of real; - /// Возвращает данные строкового столбца по имени - /// Не изменяйте их, иначе DataFrame будет повреждён + /// Возвращает внутренний буфер строкового столбца без копирования. + /// Не изменяйте его, иначе DataFrame будет повреждён. function GetStrColumn(name: string): array of string; - /// Возвращает данные логического столбца по имени - /// Не изменяйте их, иначе DataFrame будет повреждён + /// Возвращает внутренний буфер логического столбца без копирования. + /// Не изменяйте его, иначе DataFrame будет повреждён. function GetBoolColumn(name: string): array of boolean; /// Вычисляет сумму значений столбца по индексу @@ -243,9 +280,9 @@ type function GroupBy(colNames: array of string): IGroupByContext; /// Возвращает первые n строк - function Head(n: integer): DataFrame; + function Head(n: integer := 10): DataFrame; /// Возвращает последние n строк - function Tail(n: integer): DataFrame; + function Tail(n: integer := 10): DataFrame; /// Фильтрует строки по предикату function Filter(pred: CursorPredicate): DataFrame; @@ -292,17 +329,26 @@ type /// Пропущенные значения (NA) сохраняются function ReplaceColumnInt(colName: string; f: DataFrameCursor -> integer): DataFrame; - /// Преобразует значения целочисленного столбца. + /// Преобразует сырые значения целочисленного столбца. /// Возвращает новый DataFrame. + function MapIntColumnData(name: string; f: integer -> integer): DataFrame; + /// Преобразует сырые значения вещественного столбца. + /// Возвращает новый DataFrame. + function MapFloatColumnData(name: string; f: real -> real): DataFrame; + /// Преобразует сырые значения строкового столбца. + /// Возвращает новый DataFrame. + function MapStrColumnData(name: string; f: string -> string): DataFrame; + /// Преобразует сырые значения логического столбца. + /// Возвращает новый DataFrame. + function MapBoolColumnData(name: string; f: boolean -> boolean): DataFrame; + + /// Устаревшее имя. Используйте MapIntColumnData. function TransformIntColumn(name: string; f: integer -> integer): DataFrame; - /// Преобразует значения вещественного столбца. - /// Возвращает новый DataFrame. + /// Устаревшее имя. Используйте MapFloatColumnData. function TransformFloatColumn(name: string; f: real -> real): DataFrame; - /// Преобразует значения строкового столбца. - /// Возвращает новый DataFrame. + /// Устаревшее имя. Используйте MapStrColumnData. function TransformStrColumn(name: string; f: string -> string): DataFrame; - /// Преобразует значения логического столбца. - /// Возвращает новый DataFrame. + /// Устаревшее имя. Используйте MapBoolColumnData. function TransformBoolColumn(name: string; f: boolean -> boolean): DataFrame; /// Возвращает новый DataFrame со строками с заданными номерами из исходного DataFrame @@ -321,20 +367,12 @@ type /// Соединяет с другим DataFrame по разным именам ключей function Join(other: DataFrame; leftKeys, rightKeys: array of string; kind: JoinKind := jkInner): DataFrame; - /// Выводит DataFrame - procedure Print(decimals: integer := 3); - /// Выводит DataFrame и переходит на новую строку - procedure Println(decimals: integer := 3); /// Выводит DataFrame с настраиваемым числом строк - procedure PrintPreview(maxRows: integer; headRows: integer := -1; decimals: integer := 3); - /// Выводит DataFrame с настраиваемым числом строк и переходит на новую строку - procedure PrintlnPreview(maxRows: integer; headRows: integer := -1; decimals: integer := 3); + procedure Print(maxRows: integer := 10; headRows: integer := -1; decimals: integer := 2); /// Выводит схему датафрейма procedure PrintSchema; /// Выводит размер, схему и количество валидных значений procedure PrintInfo; - /// Выводит размер, схему и количество валидных значений - procedure PrintlnInfo; /// Загружает DataFrame из CSV файла static function FromCsv(filename: string): DataFrame; @@ -505,6 +543,12 @@ const 'schema не может быть nil!!schema cannot be nil'; ER_COLS_SCHEMA_MISMATCH = 'Количество столбцов не совпадает со схемой!!Columns count and schema mismatch'; + ER_SCHEMA_NAME_MISMATCH = + 'Имя столбца #{0} не совпадает со схемой: "{1}" vs "{2}"!!Column name #{0} does not match schema: "{1}" vs "{2}"'; + ER_SCHEMA_TYPE_MISMATCH = + 'Тип столбца "{0}" не совпадает со схемой: {1} vs {2}!!Column type "{0}" does not match schema: {1} vs {2}'; + ER_COLUMN_ROWCOUNT_MISMATCH = + 'Длина столбца "{0}" не совпадает с ожидаемой: {1} vs {2}!!Column "{0}" row count does not match expected: {1} vs {2}'; ER_JOIN_KEY_TYPE_MISMATCH = 'Типы ключей соединения не совпадают!!Join key types mismatch'; ER_JOIN_KEY_NOT_FOUND = @@ -930,7 +974,7 @@ begin // --- right columns (учитывают -1) for var ci := 0 to other.ColumnCount - 1 do if ci <> rightKey then - BuildColumnFromJoin(res, 'right_' + other.columns[ci].Info.Name, other.columns[ci], rightArr); + BuildColumnFromJoin(res, MergedRightColumnName(Self.fSchema, other.fSchema, ci), other.columns[ci], rightArr); var schema := DataFrameSchema.Merge( Self.fSchema, @@ -955,16 +999,6 @@ begin fschema := new DataFrameSchema([], []); end; -constructor DataFrame.Create(cols: List); -begin - if cols = nil then - ArgumentNullError(ER_COLS_NULL); - - self.columns := cols; - - RebuildSchema; -end; - constructor DataFrame.Create(cols: List; schema: DataFrameSchema); begin if cols = nil then @@ -975,9 +1009,13 @@ begin ArgumentError(ER_COLS_SCHEMA_MISMATCH); self.columns := cols; - self.fSchema := schema; - - RebuildSchema; + self.fSchema := new DataFrameSchema( + schema.ColumnNames, + schema.Types, + schema.CategoricalFlags + ); + + ValidateColumnsAgainstSchema; end; function DataFrame.BuildJoinKey(cur: DataFrameCursor; layout: JoinKeyLayout; var hasNA: boolean): JoinKey; @@ -1111,8 +1149,9 @@ begin var k := lcur.Int(leftKey); - if index.ContainsKey(k) then - foreach var r in index[k] do + var rows: List; + if index.TryGetValue(k, rows) then + foreach var r in rows do begin leftIdx.Add(lpos); rightIdx.Add(r); @@ -1164,8 +1203,9 @@ begin var k := lcur.Str(leftKey); - if index.ContainsKey(k) then - foreach var r in index[k] do + var rows: List; + if index.TryGetValue(k, rows) then + foreach var r in rows do begin leftIdx.Add(lpos); rightIdx.Add(r); @@ -1217,8 +1257,9 @@ begin var k := lcur.Bool(leftKey); - if index.ContainsKey(k) then - foreach var r in index[k] do + var rows: List; + if index.TryGetValue(k, rows) then + foreach var r in rows do begin leftIdx.Add(lpos); rightIdx.Add(r); @@ -1281,8 +1322,9 @@ begin var hasNA := false; var key := BuildJoinKey(lcur, leftLayout, hasNA); - if (not hasNA) and hash.ContainsKey(key) then - foreach var rpos in hash[key] do + var rows: List; + if (not hasNA) and hash.TryGetValue(key, rows) then + foreach var rpos in rows do begin leftIdx.Add(lpos); rightIdx.Add(rpos); @@ -1306,7 +1348,7 @@ begin // --- right columns (exclude keys) for var ci := 0 to other.ColumnCount - 1 do if not rightKeyIdx.Contains(ci) then - BuildColumnFromJoin(res, 'right_' + other.columns[ci].Info.Name, other.columns[ci], rightArr); + BuildColumnFromJoin(res, MergedRightColumnName(fSchema, other.fSchema, ci), other.columns[ci], rightArr); // --- schema res.SetSchema(schema); @@ -1466,7 +1508,7 @@ begin // --- 9. right колонки (без ключа) for var ci := 0 to other.ColumnCount - 1 do if ci <> ri then - BuildColumnFromJoin(res, 'right_' + other.columns[ci].Info.Name, other.columns[ci], rightArr); + BuildColumnFromJoin(res, MergedRightColumnName(fSchema, other.fSchema, ci), other.columns[ci], rightArr); // --- 10. схема res.SetSchema(schema); @@ -1600,7 +1642,7 @@ begin // --- 9. right колонки (без ключей) for var ci := 0 to other.ColumnCount - 1 do if not rightKeyIdx.Contains(ci) then - BuildColumnFromJoin(res, 'right_' + other.columns[ci].Info.Name, other.columns[ci], rightArr); + BuildColumnFromJoin(res, MergedRightColumnName(fSchema, other.fSchema, ci), other.columns[ci], rightArr); // --- 10. схема res.SetSchema(schema); @@ -1650,7 +1692,7 @@ begin // --- right columns (exclude key) for var ci := 0 to other.ColumnCount - 1 do if ci <> rightKey then - BuildColumnFromJoin(res, 'right_' + other.columns[ci].Info.Name, other.columns[ci], rightArr); + BuildColumnFromJoin(res, MergedRightColumnName(Self.fSchema, other.fSchema, ci), other.columns[ci], rightArr); var schema := DataFrameSchema.Merge( Self.fSchema, @@ -1850,7 +1892,7 @@ begin // --- right columns (exclude keys) for var ci := 0 to other.ColumnCount - 1 do if not rightKeyIdx.Contains(ci) then - BuildColumnFromJoin(res, 'right_' + other.columns[ci].Info.Name, other.columns[ci], rightArr); + BuildColumnFromJoin(res, MergedRightColumnName(fSchema, other.fSchema, ci), other.columns[ci], rightArr); // --- schema var schema := DataFrameSchema.Merge( @@ -1872,7 +1914,7 @@ begin for var i := 0 to keyIndices.Length - 1 do begin - var t := columns[keyIndices[i]].Info.ColType; + var t := fSchema.ColumnTypeAt(keyIndices[i]); if t = ctFloat then Error(ER_JOIN_FLOAT_KEY_NOT_SUPPORTED); @@ -1922,6 +1964,197 @@ begin if leftKeys.Length <> rightKeys.Length then ArgumentError(ER_JOIN_KEYS_LENGTH_MISMATCH); + if leftKeys.Length = 1 then + begin + var leftKey := fSchema.IndexOf(leftKeys[0]); + var rightKey := other.fSchema.IndexOf(rightKeys[0]); + var lt := fSchema.ColumnTypeAt(leftKey); + var rt := other.fSchema.ColumnTypeAt(rightKey); + + if lt <> rt then + Error(ER_JOIN_KEY_TYPE_MISMATCH); + + if lt = ctFloat then + Error(ER_JOIN_FLOAT_KEY_NOT_SUPPORTED); + + case kind of + jkInner: + case lt of + ctInt: exit(JoinInnerSingleKeyInt(other, leftKey, rightKey)); + ctStr: exit(JoinInnerSingleKeyStr(other, leftKey, rightKey)); + ctBool: exit(JoinInnerSingleKeyBool(other, leftKey, rightKey)); + else + Error(ER_UNSUPPORTED_COLUMN_TYPE, lt); + end; + + jkLeft: + case lt of + ctInt: exit(LeftJoinSingleKeyInt(other, leftKey, rightKey)); + ctStr: exit(LeftJoinSingleKeyStr(other, leftKey, rightKey)); + ctBool: exit(LeftJoinSingleKeyBool(other, leftKey, rightKey)); + else + Error(ER_UNSUPPORTED_COLUMN_TYPE, lt); + end; + + jkRight: + begin + case lt of + ctInt: + begin + var index := new Dictionary>; + var lcur := GetCursor; + while lcur.MoveNext do + if lcur.IsValid(leftKey) then + begin + var k := lcur.Int(leftKey); + var lst: List; + if not index.TryGetValue(k, lst) then + begin + lst := new List; + index[k] := lst; + end; + lst.Add(lcur.Position); + end; + + var leftIdx := new List; + var rightIdx := new List; + var rcur := other.GetCursor; + while rcur.MoveNext do + begin + var rpos := rcur.Position; + if not rcur.IsValid(rightKey) then + begin + leftIdx.Add(-1); + rightIdx.Add(rpos); + continue; + end; + + var k := rcur.Int(rightKey); + var rows: List; + if index.TryGetValue(k, rows) then + foreach var lpos in rows do + begin + leftIdx.Add(lpos); + rightIdx.Add(rpos); + end + else + begin + leftIdx.Add(-1); + rightIdx.Add(rpos); + end; + end; + exit(BuildLeftJoinResult(Self, other, leftIdx, rightIdx, leftKey, rightKey)); + end; + + ctStr: + begin + var index := new Dictionary>; + var lcur := GetCursor; + while lcur.MoveNext do + if lcur.IsValid(leftKey) then + begin + var k := lcur.Str(leftKey); + var lst: List; + if not index.TryGetValue(k, lst) then + begin + lst := new List; + index[k] := lst; + end; + lst.Add(lcur.Position); + end; + + var leftIdx := new List; + var rightIdx := new List; + var rcur := other.GetCursor; + while rcur.MoveNext do + begin + var rpos := rcur.Position; + if not rcur.IsValid(rightKey) then + begin + leftIdx.Add(-1); + rightIdx.Add(rpos); + continue; + end; + + var k := rcur.Str(rightKey); + var rows: List; + if index.TryGetValue(k, rows) then + foreach var lpos in rows do + begin + leftIdx.Add(lpos); + rightIdx.Add(rpos); + end + else + begin + leftIdx.Add(-1); + rightIdx.Add(rpos); + end; + end; + exit(BuildLeftJoinResult(Self, other, leftIdx, rightIdx, leftKey, rightKey)); + end; + + ctBool: + begin + var index := new Dictionary>; + var lcur := GetCursor; + while lcur.MoveNext do + if lcur.IsValid(leftKey) then + begin + var k := lcur.Bool(leftKey); + var lst: List; + if not index.TryGetValue(k, lst) then + begin + lst := new List; + index[k] := lst; + end; + lst.Add(lcur.Position); + end; + + var leftIdx := new List; + var rightIdx := new List; + var rcur := other.GetCursor; + while rcur.MoveNext do + begin + var rpos := rcur.Position; + if not rcur.IsValid(rightKey) then + begin + leftIdx.Add(-1); + rightIdx.Add(rpos); + continue; + end; + + var k := rcur.Bool(rightKey); + var rows: List; + if index.TryGetValue(k, rows) then + foreach var lpos in rows do + begin + leftIdx.Add(lpos); + rightIdx.Add(rpos); + end + else + begin + leftIdx.Add(-1); + rightIdx.Add(rpos); + end; + end; + exit(BuildLeftJoinResult(Self, other, leftIdx, rightIdx, leftKey, rightKey)); + end; + + else + Error(ER_UNSUPPORTED_COLUMN_TYPE, lt); + end; + end; + + jkFull: + begin + var tmp := other; + if leftKeys[0] <> rightKeys[0] then + tmp := other.Rename([(rightKeys[0], leftKeys[0])]); + exit(FullJoinSingleKey(tmp, leftKeys[0])); + end; + end; + end; + // временно переименовываем столбцы справа var tmp := other; var renames := new List<(string,string)>; @@ -1960,9 +2193,71 @@ begin fSchema := new DataFrameSchema(names, types, cats); end; +procedure DataFrame.CommitAddedColumn(c: Column); +begin + var n := columns.Count; + var names := new string[n + 1]; + var types := new ColumnType[n + 1]; + var cats := new boolean[n + 1]; + + for var i := 0 to n - 1 do + begin + var info := columns[i].Info; + names[i] := info.Name; + types[i] := info.ColType; + if fSchema <> nil then + cats[i] := fSchema.CategoricalFlags[i] + else + cats[i] := false; + end; + + names[n] := c.Info.Name; + types[n] := c.Info.ColType; + cats[n] := false; + + var newSchema := new DataFrameSchema(names, types, cats); + columns.Add(c); + fSchema := newSchema; +end; + +procedure DataFrame.ValidateColumnsAgainstSchema; +begin + ValidateColumnsAgainstSchema(fSchema); +end; + +procedure DataFrame.ValidateColumnsAgainstSchema(candidate: DataFrameSchema); +begin + if columns = nil then + ArgumentNullError(ER_COLS_NULL); + + if candidate = nil then + ArgumentNullError(ER_SCHEMA_NULL); + + if columns.Count <> candidate.ColumnCount then + ArgumentError(ER_COLS_SCHEMA_MISMATCH); + + var expectedRowCount := if columns.Count = 0 then 0 else columns[0].RowCount; + + for var i := 0 to columns.Count - 1 do + begin + var info := columns[i].Info; + var schemaName := candidate.NameAt(i); + var schemaType := candidate.ColumnTypeAt(i); + + if columns[i].RowCount <> expectedRowCount then + ArgumentError(ER_COLUMN_ROWCOUNT_MISMATCH, info.Name, columns[i].RowCount, expectedRowCount); + + if info.Name <> schemaName then + ArgumentError(ER_SCHEMA_NAME_MISMATCH, i, info.Name, schemaName); + + if info.ColType <> schemaType then + ArgumentError(ER_SCHEMA_TYPE_MISMATCH, info.Name, info.ColType, schemaType); + end; +end; + function DataFrame.GetColumnType(colIndex: integer): ColumnType; begin - Result := columns[colIndex].Info.ColType + Result := fSchema.ColumnTypeAt(colIndex) end; function DataFrame.RowCount: integer; @@ -1987,6 +2282,15 @@ begin Result := fSchema.HasColumn(name); end; +function DataFrame.GetSchema: DataFrameSchema; +begin + Result := new DataFrameSchema( + fSchema.ColumnNames, + fSchema.Types, + fSchema.CategoricalFlags + ); +end; + function DataFrame.GetCursor: DataFrameCursor := new DataFrameCursor(columns.ToArray,fSchema); @@ -2018,7 +2322,6 @@ begin Result := c.Data; end; - function DataFrame.TrainTestSplit(testRatio: real; shuffle: boolean; seed: integer): (DataFrame, DataFrame); begin if Self = nil then @@ -2073,37 +2376,140 @@ begin Result := (trainDf, testDf); end; +function DataFrame.StratifiedTrainTestSplit( + target: string; + testRatio: real; + seed: integer +): (DataFrame, DataFrame); +begin + if Self = nil then + ArgumentNullError(ER_ARG_NULL, 'DataFrame'); + + if target = nil then + ArgumentNullError(ER_ARG_NULL, 'target'); + + if not HasColumn(target) then + ArgumentError(ER_COLUMN_NOT_FOUND, target); + + if (testRatio <= 0.0) or (testRatio >= 1.0) then + ArgumentError(ER_TEST_RATIO_INVALID, testRatio); + + var n := RowCount; + if n < 2 then + ArgumentError(ER_EMPTY_DATA, 'StratifiedTrainTestSplit'); + + var actualSeed := if seed >= 0 then seed else System.Environment.TickCount and integer.MaxValue; + var rnd := new System.Random(actualSeed); + + var ci := ColumnIndex(target); + var col := GetColumn(ci); + + var groups := new Dictionary>; + var keyOrder := new List; + + case col.Info.ColType of + + ctInt: + begin + var data := IntColumn(col).Data; + for var i := 0 to n - 1 do + begin + var key: object := data[i]; + + var lst: List; + if not groups.TryGetValue(key, lst) then + begin + lst := new List; + groups[key] := lst; + keyOrder.Add(key); + end; + + lst.Add(i); + end; + end; + + ctStr: + begin + var data := StrColumn(col).Data; + for var i := 0 to n - 1 do + begin + var key: object := data[i]; + + var lst: List; + if not groups.TryGetValue(key, lst) then + begin + lst := new List; + groups[key] := lst; + keyOrder.Add(key); + end; + + lst.Add(i); + end; + end; + + else + Error(ER_GROUPBY_UNSUPPORTED_KEY_TYPE, col.Info.ColType); + end; + + var trainIdx := new List; + var testIdx := new List; + + foreach var key in keyOrder do + begin + var arr := groups[key].ToArray; + arr.Shuffle(rnd); + + var m := arr.Length; + var rawSize := Round(m * testRatio); + var testSize := rawSize.Clamp(1, m - 1); + + for var i := 0 to testSize - 1 do + testIdx.Add(arr[i]); + + for var i := testSize to m - 1 do + trainIdx.Add(arr[i]); + end; + + var trainDf := TakeRows(trainIdx.ToArray); + var testDf := TakeRows(testIdx.ToArray); + + Result := (trainDf, testDf); +end; + procedure DataFrame.AddIntColumn(name: string; data: array of integer; valid: array of boolean); begin + if HasColumn(name) then + ArgumentError(ER_COLUMN_ALREADY_EXISTS, name); + if (columns.Count > 0) and (Length(data) <> RowCount) then DimensionError(ER_ADD_COLUMN_ROW_MISMATCH); var c := new IntColumn(name, data, valid); - - columns.Add(c); - RebuildSchema; + CommitAddedColumn(c); end; procedure DataFrame.AddFloatColumn(name: string; data: array of real; valid: array of boolean); begin + if HasColumn(name) then + ArgumentError(ER_COLUMN_ALREADY_EXISTS, name); + if (columns.Count > 0) and (Length(data) <> RowCount) then DimensionError(ER_ADD_COLUMN_ROW_MISMATCH); var c := new FloatColumn(name, data, valid); - - columns.Add(c); - RebuildSchema; + CommitAddedColumn(c); end; procedure DataFrame.AddStrColumn(name: string; data: array of string; valid: array of boolean); begin + if HasColumn(name) then + ArgumentError(ER_COLUMN_ALREADY_EXISTS, name); + if (columns.Count > 0) and (Length(data) <> RowCount) then DimensionError(ER_ADD_COLUMN_ROW_MISMATCH); var c := new StrColumn(name, data, valid); - - columns.Add(c); - RebuildSchema; + CommitAddedColumn(c); end; procedure DataFrame.AddStrColumn(name: string; data: array of char; valid: array of boolean); @@ -2114,13 +2520,14 @@ end; procedure DataFrame.AddBoolColumn(name: string; data: array of boolean; valid: array of boolean); begin + if HasColumn(name) then + ArgumentError(ER_COLUMN_ALREADY_EXISTS, name); + if (columns.Count > 0) and (Length(data) <> RowCount) then DimensionError(ER_ADD_COLUMN_ROW_MISMATCH); var c := new BoolColumn(name, data, valid); - - columns.Add(c); - RebuildSchema; + CommitAddedColumn(c); end; procedure DataFrame.CheckColumnIndex(colIndex: integer); @@ -2531,7 +2938,7 @@ end;} function DataFrame.Head(n: integer): DataFrame; begin if n <= 0 then - exit(new DataFrame); + exit(TakeRows([])); var k := PABCSystem.Min(n, RowCount); @@ -2541,7 +2948,7 @@ end; function DataFrame.Tail(n: integer): DataFrame; begin if n <= 0 then - exit(new DataFrame); + exit(TakeRows([])); var total := RowCount; var k := PABCSystem.Min(n, total); @@ -2591,7 +2998,7 @@ begin if not k.Valid[i] then continue; - case columns[c].Info.ColType of + case fSchema.ColumnTypeAt(c) of ctInt: k.IntVals[i] := cur.Int(c); ctFloat: k.FloatVals[i] := cur.Float(c); ctStr: k.StrVals[i] := cur.Str(c); @@ -2616,7 +3023,7 @@ begin continue; var cmp: integer; - case columns[colIndices[i]].Info.ColType of + case fSchema.ColumnTypeAt(colIndices[i]) of ctInt: cmp := a.IntVals[i].CompareTo(b.IntVals[i]); ctFloat: cmp := a.FloatVals[i].CompareTo(b.FloatVals[i]); ctStr: cmp := a.StrVals[i].CompareTo(b.StrVals[i]); @@ -2661,10 +3068,14 @@ begin if schema = nil then ArgumentNullError(ER_SCHEMA_NULL); - if schema.ColumnCount <> ColumnCount then - ArgumentError(ER_COLS_SCHEMA_MISMATCH); - - fSchema := schema; + var candidate := new DataFrameSchema( + schema.ColumnNames, + schema.Types, + schema.CategoricalFlags + ); + + ValidateColumnsAgainstSchema(candidate); + fSchema := candidate; end; function DataFrame.SetCategorical(names: array of string): DataFrame; @@ -2688,8 +3099,8 @@ begin cats ); - // создаём новый DataFrame (columns не копируем!) - Result := new DataFrame(columns, newSchema); + // создаём новый DataFrame с отдельным списком колонок + Result := new DataFrame(columns.ToList, newSchema); end; function DataFrame.IsCategorical(name: string): boolean; @@ -2841,8 +3252,7 @@ begin if oldName = newName then exit(Self); - var newSchema := fSchema.Rename(oldName, newName); - Result := new DataFrame(columns, newSchema); + Result := Rename([(oldName, newName)]); end; @@ -2866,7 +3276,7 @@ begin var oldName := col.Info.Name; var newName := if map.ContainsKey(oldName) then map[oldName] else oldName; - case col.Info.ColType of + case fSchema.ColumnTypeAt(i) of ctInt: begin var c := IntColumn(col); @@ -2996,7 +3406,7 @@ begin Result := columns[idx]; end; -function DataFrame.CloneWithCopiedColumns: DataFrame; +function DataFrame.CloneWithSharedColumns: DataFrame; begin Result := new DataFrame; @@ -3004,7 +3414,7 @@ begin begin var col := columns[i]; - case col.Info.ColType of + case fSchema.ColumnTypeAt(i) of ctInt: begin var c := IntColumn(col); @@ -3037,7 +3447,7 @@ begin if fSchema.HasColumn(name) then ArgumentError(ER_COLUMN_ALREADY_EXISTS, name); - var res := CloneWithCopiedColumns; + var res := CloneWithSharedColumns; var data := new integer[RowCount]; var valid := new boolean[RowCount]; @@ -3064,7 +3474,7 @@ begin if fSchema.HasColumn(name) then ArgumentError(ER_COLUMN_ALREADY_EXISTS, name); - var res := CloneWithCopiedColumns; + var res := CloneWithSharedColumns; var data := new real[RowCount]; var valid := new boolean[RowCount]; @@ -3091,7 +3501,7 @@ begin if fSchema.HasColumn(name) then ArgumentError(ER_COLUMN_ALREADY_EXISTS, name); - var res := CloneWithCopiedColumns; + var res := CloneWithSharedColumns; var data := new string[RowCount]; var valid := new boolean[RowCount]; @@ -3118,7 +3528,7 @@ begin if fSchema.HasColumn(name) then ArgumentError(ER_COLUMN_ALREADY_EXISTS, name); - var res := CloneWithCopiedColumns; + var res := CloneWithSharedColumns; var data := new boolean[RowCount]; var valid := new boolean[RowCount]; @@ -3306,12 +3716,13 @@ begin res.AddColumnAlias(columns[i]); res.AddIntColumn(name, data, valid); + res.SetSchema(ExtendSchema(name, ctInt, false)); Result := res; Result.AssertSchemaConsistent; end; -function DataFrame.TransformIntColumn(name: string; f: integer -> integer): DataFrame; +function DataFrame.MapIntColumnData(name: string; f: integer -> integer): DataFrame; begin var res := new DataFrame; @@ -3358,12 +3769,21 @@ begin end; end; + res.SetSchema(new DataFrameSchema( + fSchema.ColumnNames, + fSchema.Types, + fSchema.CategoricalFlags + )); + Result := res; Result.AssertSchemaConsistent; end; -function DataFrame.TransformFloatColumn(name: string; f: real -> real): DataFrame; +function DataFrame.TransformIntColumn(name: string; f: integer -> integer): DataFrame := + MapIntColumnData(name, f); + +function DataFrame.MapFloatColumnData(name: string; f: real -> real): DataFrame; begin var res := new DataFrame; @@ -3410,12 +3830,21 @@ begin end; end; + res.SetSchema(new DataFrameSchema( + fSchema.ColumnNames, + fSchema.Types, + fSchema.CategoricalFlags + )); + Result := res; Result.AssertSchemaConsistent; end; -function DataFrame.TransformStrColumn(name: string; f: string -> string): DataFrame; +function DataFrame.TransformFloatColumn(name: string; f: real -> real): DataFrame := + MapFloatColumnData(name, f); + +function DataFrame.MapStrColumnData(name: string; f: string -> string): DataFrame; begin var res := new DataFrame; @@ -3462,12 +3891,21 @@ begin end; end; + res.SetSchema(new DataFrameSchema( + fSchema.ColumnNames, + fSchema.Types, + fSchema.CategoricalFlags + )); + Result := res; Result.AssertSchemaConsistent; end; -function DataFrame.TransformBoolColumn(name: string; f: boolean -> boolean): DataFrame; +function DataFrame.TransformStrColumn(name: string; f: string -> string): DataFrame := + MapStrColumnData(name, f); + +function DataFrame.MapBoolColumnData(name: string; f: boolean -> boolean): DataFrame; begin var res := new DataFrame; @@ -3514,11 +3952,20 @@ begin end; end; + res.SetSchema(new DataFrameSchema( + fSchema.ColumnNames, + fSchema.Types, + fSchema.CategoricalFlags + )); + Result := res; Result.AssertSchemaConsistent; end; +function DataFrame.TransformBoolColumn(name: string; f: boolean -> boolean): DataFrame := + MapBoolColumnData(name, f); + function DataFrame.TakeRows(indices: array of integer): DataFrame; begin if indices = nil then @@ -3527,6 +3974,14 @@ begin var k := indices.Length; var res := new DataFrame; + for var j := 0 to k - 1 do + begin + var i := indices[j]; + + if (i < 0) or (i >= RowCount) then + ArgumentError(ER_ROW_INDEX_OUT_OF_RANGE, i); + end; + var names := new List; var types := new List; var cats := new List; @@ -3545,16 +4000,13 @@ begin var data := new integer[k]; var validDst := new boolean[k]; - for var j := 0 to k - 1 do - begin - var i := indices[j]; + for var j := 0 to k - 1 do + begin + var i := indices[j]; - if (i < 0) or (i >= RowCount) then - ArgumentError(ER_ROW_INDEX_OUT_OF_RANGE, i); - - data[j] := src.Data[i]; - validDst[j] := src.IsValid[i]; - end; + data[j] := src.Data[i]; + validDst[j] := src.IsValid[i]; + end; res.AddIntColumn(name, data, validDst); end; @@ -3565,16 +4017,13 @@ begin var data := new real[k]; var validDst := new boolean[k]; - for var j := 0 to k - 1 do - begin - var i := indices[j]; + for var j := 0 to k - 1 do + begin + var i := indices[j]; - if (i < 0) or (i >= RowCount) then - ArgumentError(ER_ROW_INDEX_OUT_OF_RANGE, i); - - data[j] := src.Data[i]; - validDst[j] := src.IsValid[i]; - end; + data[j] := src.Data[i]; + validDst[j] := src.IsValid[i]; + end; res.AddFloatColumn(name, data, validDst); end; @@ -3585,16 +4034,13 @@ begin var data := new string[k]; var validDst := new boolean[k]; - for var j := 0 to k - 1 do - begin - var i := indices[j]; + for var j := 0 to k - 1 do + begin + var i := indices[j]; - if (i < 0) or (i >= RowCount) then - ArgumentError(ER_ROW_INDEX_OUT_OF_RANGE, i); - - data[j] := src.Data[i]; - validDst[j] := src.IsValid[i]; - end; + data[j] := src.Data[i]; + validDst[j] := src.IsValid[i]; + end; res.AddStrColumn(name, data, validDst); end; @@ -3605,16 +4051,13 @@ begin var data := new boolean[k]; var validDst := new boolean[k]; - for var j := 0 to k - 1 do - begin - var i := indices[j]; + for var j := 0 to k - 1 do + begin + var i := indices[j]; - if (i < 0) or (i >= RowCount) then - ArgumentError(ER_ROW_INDEX_OUT_OF_RANGE, i); - - data[j] := src.Data[i]; - validDst[j] := src.IsValid[i]; - end; + data[j] := src.Data[i]; + validDst[j] := src.IsValid[i]; + end; res.AddBoolColumn(name, data, validDst); end; @@ -3637,7 +4080,7 @@ begin Result := res; end; -procedure DataFrame.PrintPreview(maxRows: integer; headRows: integer; decimals: integer); +procedure DataFrame.Print(maxRows: integer; headRows: integer; decimals: integer); begin var colCount := columns.Count; if colCount = 0 then exit; @@ -3810,23 +4253,6 @@ begin end; end; -procedure DataFrame.PrintlnPreview(maxRows: integer; headRows: integer; decimals: integer); -begin - PrintPreview(maxRows, headRows, decimals); - PABCSystem.Println; -end; - -procedure DataFrame.Print(decimals: integer); -begin - PrintPreview(10, 5, decimals); -end; - -procedure DataFrame.Println(decimals: integer); -begin - Print(decimals); - PABCSystem.Println; -end; - procedure DataFrame.PrintSchema; begin var nameWidth := fSchema.ColumnNames.Max(s -> s.Length); @@ -3887,12 +4313,6 @@ begin end; end; -procedure DataFrame.PrintlnInfo; -begin - PrintInfo; - PABCSystem.Println -end; - procedure DataFrame.AssertSchemaConsistent; begin {$IFNDEF Test} @@ -3913,6 +4333,14 @@ begin for var i := 0 to columns.Count - 1 do begin var name := columns[i].Info.Name; + var schemaName := fSchema.NameAt(i); + var schemaType := fSchema.ColumnTypeAt(i); + + if name <> schemaName then + Error(ER_SCHEMA_NAME_MISMATCH, i, name, schemaName); + + if columns[i].Info.ColType <> schemaType then + Error(ER_SCHEMA_TYPE_MISMATCH, name, columns[i].Info.ColType, schemaType); if not fSchema.HasColumn(name) then Error(ER_SCHEMA_COLUMN_MISSING, name); @@ -4459,7 +4887,7 @@ begin var col := source.columns[ci]; var colName := col.Info.Name; - case col.Info.ColType of + case source.fSchema.ColumnTypeAt(ci) of ctInt: begin res.AddIntColumn(colName, keys.Select(key -> integer(key.Values[k])).ToArray, nil); @@ -4612,9 +5040,9 @@ begin // собираем все числовые колонки for var i := 0 to source.ColumnCount - 1 do - case source.columns[i].Info.ColType of + case source.fSchema.ColumnTypeAt(i) of ctInt, ctFloat: - cols.Add(source.columns[i].Info.Name); + cols.Add(source.fSchema.NameAt(i)); end; if cols.Count = 0 then @@ -4857,13 +5285,19 @@ begin var col := source.columns[keyColumn]; var keyName := col.Info.Name; - if col.Info.ColType = ctInt then - res.AddIntColumn(keyName, keys1.Select(k -> integer(k)).ToArray, nil) + case source.fSchema.ColumnTypeAt(keyColumn) of + ctInt: + res.AddIntColumn(keyName, keys1.Select(k -> integer(k)).ToArray, nil); + ctStr: + res.AddStrColumn(keyName, keys1.Select(k -> string(k)).ToArray, nil); + ctBool: + res.AddBoolColumn(keyName, keys1.Select(k -> boolean(k)).ToArray, nil); else - res.AddStrColumn(keyName, keys1.Select(k -> string(k)).ToArray, nil); + Error(ER_GROUPBY_UNSUPPORTED_KEY_TYPE, source.fSchema.ColumnTypeAt(keyColumn)); + end; names.Add(keyName); - types.Add(col.Info.ColType); + types.Add(source.fSchema.ColumnTypeAt(keyColumn)); cats.Add(true); end else @@ -4874,13 +5308,19 @@ begin var col := source.columns[ci]; var keyName := col.Info.Name; - if col.Info.ColType = ctInt then - res.AddIntColumn(keyName, keysN.Select(key -> integer(key.Values[k])).ToArray, nil) + case source.fSchema.ColumnTypeAt(ci) of + ctInt: + res.AddIntColumn(keyName, keysN.Select(key -> integer(key.Values[k])).ToArray, nil); + ctStr: + res.AddStrColumn(keyName, keysN.Select(key -> string(key.Values[k])).ToArray, nil); + ctBool: + res.AddBoolColumn(keyName, keysN.Select(key -> boolean(key.Values[k])).ToArray, nil); else - res.AddStrColumn(keyName, keysN.Select(key -> string(key.Values[k])).ToArray, nil); + Error(ER_GROUPBY_UNSUPPORTED_KEY_TYPE, source.fSchema.ColumnTypeAt(ci)); + end; names.Add(keyName); - types.Add(col.Info.ColType); + types.Add(source.fSchema.ColumnTypeAt(ci)); cats.Add(true); end; end; @@ -5222,9 +5662,10 @@ begin var names := oldSchema.ColumnNames; var cats := oldSchema.CategoricalFlags; - var types := new ColumnType[names.Length]; + var types := Copy(oldSchema.Types); for var i := 0 to names.Length - 1 do - types[i] := ctFloat; + if isNumeric[i] then + types[i] := ctFloat; res.SetSchema(new DataFrameSchema(names, types, cats)); @@ -5308,9 +5749,10 @@ begin var names := oldSchema.ColumnNames; var cats := oldSchema.CategoricalFlags; - var types := new ColumnType[names.Length]; + var types := Copy(oldSchema.Types); for var i := 0 to names.Length - 1 do - types[i] := ctFloat; + if isNumeric[i] then + types[i] := ctFloat; res.SetSchema(new DataFrameSchema(names, types, cats)); @@ -6333,4 +6775,4 @@ begin end; end; -end. \ No newline at end of file +end. diff --git a/bin/Lib/DataFrameABCCore.pas b/bin/Lib/DataFrameABCCore.pas index d7f51203f..8e3337acf 100644 --- a/bin/Lib/DataFrameABCCore.pas +++ b/bin/Lib/DataFrameABCCore.pas @@ -35,11 +35,14 @@ type fIndexByName: Dictionary; class function BuildIndex(names: array of string): Dictionary; + function GetColumnNames: array of string; + function GetTypes: array of ColumnType; + function GetCategoricalFlags: array of boolean; public property ColumnCount: integer read fNames.Length; - property ColumnNames: array of string read fNames; - property Types: array of ColumnType read fTypes; - property CategoricalFlags: array of boolean read fCategoricalFlags; + property ColumnNames: array of string read GetColumnNames; + property Types: array of ColumnType read GetTypes; + property CategoricalFlags: array of boolean read GetCategoricalFlags; function IndexOf(name: string): integer; function HasColumn(name: string): boolean; @@ -79,9 +82,14 @@ type procedure AssertConsistent; end; - ColumnInfo = auto class - Name: string; - ColType: ColumnType; + ColumnInfo = sealed class + private + fName: string; + fColType: ColumnType; + public + property Name: string read fName; + property ColType: ColumnType read fColType; + constructor Create(name: string; colType: ColumnType); //IsCategorical - только в Schema! end; @@ -178,7 +186,7 @@ type function Equals(oth: object): boolean; override; function GetHashCode: integer; override; end; - + /// Курсор для итерации по строкам DataFrame DataFrameCursor = class private @@ -243,6 +251,8 @@ type /// Максимальное значение Max: real; end; + +function MergedRightColumnName(leftSchema, rightSchema: DataFrameSchema; rightIndex: integer): string; implementation @@ -331,6 +341,21 @@ begin end; end; +function DataFrameSchema.GetColumnNames: array of string; +begin + Result := Copy(fNames); +end; + +function DataFrameSchema.GetTypes: array of ColumnType; +begin + Result := Copy(fTypes); +end; + +function DataFrameSchema.GetCategoricalFlags: array of boolean; +begin + Result := Copy(fCategoricalFlags); +end; + constructor DataFrameSchema.Create(names: array of string; types: array of ColumnType; isCategorical: array of boolean); begin @@ -357,6 +382,12 @@ begin AssertConsistent; end; +constructor ColumnInfo.Create(name: string; colType: ColumnType); +begin + fName := name; + fColType := colType; +end; + procedure DataFrameSchema.Print; begin if fNames.Length = 0 then @@ -548,6 +579,13 @@ begin Result := new DataFrameSchema(names.ToArray, types.ToArray, cats.ToArray); end; +function MergedRightColumnName(leftSchema, rightSchema: DataFrameSchema; rightIndex: integer): string; +begin + Result := rightSchema.NameAt(rightIndex); + if leftSchema.HasColumn(Result) then + Result := 'right_' + Result; +end; + procedure DataFrameSchema.AssertConsistent; begin Assert(fNames.Length = fTypes.Length); diff --git a/bin/Lib/LinearAlgebraML.pas b/bin/Lib/LinearAlgebraML.pas index 008f26d11..5846a1490 100644 --- a/bin/Lib/LinearAlgebraML.pas +++ b/bin/Lib/LinearAlgebraML.pas @@ -32,20 +32,31 @@ type static procedure CheckNonEmpty(const v: Vector); procedure SetData(i: integer; value: real) := fdata[i] := value; public + /// Массив элементов вектора. property Data: array of real read fdata; + /// Длина вектора. property Length: integer read fdata.Length; + /// Доступ к элементу вектора по индексу. property Item[i: integer]: real read fdata[i] write SetData; default; + /// Создаёт вектор заданной длины, заполненный нулями. constructor Create(n: integer); + /// Создаёт вектор из массива вещественных значений. constructor Create(values: array of real); + /// Создаёт вектор из массива целых значений. constructor Create(values: array of integer); + /// Возвращает копию вектора. function Clone: Vector; + /// Нормирует текущий вектор по евклидовой норме и возвращает его. function Normalize: Vector; + /// Возвращает нормированную копию вектора. function Normalized: Vector; + /// Возвращает копию данных в виде массива. function ToArray: array of real; + /// Преобразует элементы вектора в массив целых чисел. function ToIntArray: array of integer; static function operator +(a, b: Vector): Vector; @@ -68,30 +79,44 @@ type // ---------- Векторные функции ---------- /// Применить функцию ко всем элементам вектора function Apply(f: real -> real): Vector; + /// Возвращает вектор из квадратных корней элементов. function Sqrt: Vector; + /// Возвращает вектор из экспонент элементов. function Exp: Vector := Apply(PABCSystem.Exp); + /// Возвращает вектор из натуральных логарифмов элементов. function Ln: Vector := Apply(PABCSystem.Ln); + /// Возвращает вектор из модулей элементов. function Abs: Vector := Apply(PABCSystem.Abs); // ---------- Основные методы ---------- + /// Возвращает сумму элементов вектора. function Sum: real; /// Среднее. Синоним Mean function Average: real; /// Среднее function Mean: real; + /// Возвращает квадрат евклидовой нормы вектора. function Norm2: real; + /// Возвращает евклидову норму вектора. function Norm: real; + /// Возвращает максимальный элемент вектора. function Max: real; + /// Возвращает минимальный элемент вектора. function Min: real; /// Скалярное произведение function Dot(b: Vector): real; // ---------- Сервисные методы ---------- + /// Возвращает строковое представление вектора. function ToString: string; override := $'{fdata.Select(x -> x.ToString(''G3''))}'; + /// Возвращает строковое представление вектора с заданной точностью. function ToString(digits: integer): string := $'{fdata.Select(x -> x.ToString(''G''+digits))}'; + /// Печатает вектор без перевода строки. procedure Print := fdata.Print; + /// Печатает вектор с переводом строки. procedure Println := fdata.Println; + /// Возвращает подмножество элементов по заданным индексам. function SubvectorBy(indices: array of integer): Vector; end; @@ -105,56 +130,97 @@ type procedure SetData(i, j: integer; value: real) := fdata[i, j] := value; public + /// Число строк матрицы. property RowCount: integer read fdata.RowCount; + /// Число столбцов матрицы. property ColCount: integer read fdata.ColCount; + /// Данные матрицы в виде двумерного массива. property Data: array[,] of real read fdata; + /// Доступ к элементу матрицы по индексам строки и столбца. property Item[i, j: integer]: real read fdata[i, j] write SetData; default; + /// Создаёт матрицу заданного размера, заполненную нулями. constructor Create(r, c: integer); + /// Создаёт матрицу из двумерного массива вещественных значений. constructor Create(values: array[,] of real); + /// Возвращает копию данных в виде двумерного массива. function ToArray2D: array[,] of real; + /// Возвращает строку матрицы в виде массива. function RowToArray(r: integer): array of real; + /// Возвращает копию матрицы. function Clone: Matrix; + /// Возвращает строку матрицы в виде массива. function Row(i: integer): array of real := fdata.Row(i); + /// Возвращает столбец матрицы в виде массива. function Col(j: integer): array of real := fdata.Col(j); + /// Возвращает суммы по всем столбцам. function ColumnSums: Vector; + /// Возвращает суммы по всем строкам. function RowSums: Vector; + /// Возвращает средние значения по всем столбцам. function ColumnMeans: Vector; + /// Возвращает средние значения по всем строкам. function RowMeans: Vector; + /// Возвращает дисперсии по всем столбцам. function ColumnVariances: Vector; + /// Возвращает дисперсии по всем строкам. function RowVariances: Vector; + /// Возвращает стандартные отклонения по всем столбцам. function ColumnStd: Vector; + /// Возвращает стандартные отклонения по всем строкам. function RowStd: Vector; + /// Возвращает минимумы по всем столбцам. function ColumnMins: Vector; + /// Возвращает максимумы по всем столбцам. function ColumnMaxs: Vector; + /// Возвращает минимумы по всем строкам. function RowMins: Vector; + /// Возвращает максимумы по всем строкам. function RowMaxs: Vector; + /// Возвращает сумму элементов строки. function RowSum(i: integer): real; + /// Возвращает среднее значение элементов строки. function RowMean(i: integer): real; + /// Возвращает дисперсию элементов строки. function RowVariance(i: integer): real; + /// Возвращает стандартное отклонение элементов строки. function RowStd(i: integer): real; + /// Возвращает минимальный элемент строки. function RowMin(i: integer): real; + /// Возвращает максимальный элемент строки. function RowMax(i: integer): real; + /// Возвращает индекс минимального элемента строки. function RowArgMin(i: integer): integer; + /// Возвращает индекс максимального элемента строки. function RowArgMax(i: integer): integer; + /// Возвращает сумму элементов столбца. function ColumnSum(j: integer): real; + /// Возвращает среднее значение элементов столбца. function ColumnMean(j: integer): real; + /// Возвращает дисперсию элементов столбца. function ColumnVariance(j: integer): real; + /// Возвращает стандартное отклонение элементов столбца. function ColumnStd(j: integer): real; + /// Возвращает минимальный элемент столбца. function ColumnMin(j: integer): real; + /// Возвращает максимальный элемент столбца. function ColumnMax(j: integer): real; + /// Возвращает индекс минимального элемента столбца. function ColumnArgMin(j: integer): integer; + /// Возвращает индекс максимального элемента столбца. function ColumnArgMax(j: integer): integer; + /// Возвращает норму Фробениуса матрицы. function FrobeniusNorm: real; + /// Прибавляет lambda к диагональным элементам матрицы. procedure AddScaledIdentity(lambda: real); @@ -206,13 +272,19 @@ type function PCA(k: integer): (Matrix, Vector); // ---------- Сервисные методы ---------- + /// Возвращает строковое представление матрицы. function ToString: string; override := $'{fdata}'; + /// Печатает матрицу без перевода строки. procedure Print := fdata.Print; + /// Печатает матрицу с переводом строки. procedure Println := fdata.Println; + /// Возвращает строку матрицы в виде вектора. function GetRow(i: integer): Vector; + /// Возвращает столбец матрицы в виде вектора. function GetCol(j: integer): Vector; + /// Возвращает матрицу, составленную из строк с заданными индексами. function TakeRows(indices: array of integer): Matrix; // ---------- Статические методы ---------- diff --git a/bin/Lib/MLABC.pas b/bin/Lib/MLABC.pas index 8d9bde51d..60e254b3c 100644 --- a/bin/Lib/MLABC.pas +++ b/bin/Lib/MLABC.pas @@ -58,6 +58,7 @@ type Matrix = LinearAlgebraML.Matrix; Validation = ValidationML.Validation; + GridSearch = ValidationML.GridSearch; Metrics = MetricsABC.Metrics; ClassificationMetrics = MetricsABC.ClassificationMetrics; @@ -69,6 +70,7 @@ type DataFrame = DataFrameABC.DataFrame; DataFrameCursor = DataFrameABCCore.DataFrameCursor; + ColumnType = DataFrameABCCore.ColumnType; Statistics = DataFrameABC.Statistics; CsvLoader = DataFrameABC.CsvLoader; @@ -130,7 +132,7 @@ type ISupervisedModel = MLCoreABC.ISupervisedModel; IUnsupervisedModel = MLCoreABC.IUnsupervisedModel; - UPipeline = MLModelsABC.UMatrixPipeline; + UMatrixPipeline = MLModelsABC.UMatrixPipeline; UDataPipeline = MLPipelineABC.UDataPipeline; TaskKind = MLPipelineABC.TaskKind; diff --git a/bin/Lib/MLCoreABC.pas b/bin/Lib/MLCoreABC.pas index 82c44b0b3..5096d1b9c 100644 --- a/bin/Lib/MLCoreABC.pas +++ b/bin/Lib/MLCoreABC.pas @@ -108,14 +108,17 @@ type /// Интерфейс классификатора. /// Наследуется от IModel. - /// Предназначен для моделей, выполняющих классификацию (предсказание меток классов). - IClassifier = interface(ISupervisedModel) - /// Возвращает индексы классов (0,1,2,...) - function PredictLabels(X: Matrix): array of integer; - - /// Возвращает метки классов в порядке кодирования - function GetClassLabels: array of string; - + /// Предназначен для моделей, выполняющих классификацию (предсказание меток классов). + IClassifier = interface(ISupervisedModel) + /// Возвращает внутренние индексы классов (0,1,2,...). + /// Индекс i соответствует метке GetClassLabels[i]. + function PredictLabels(X: Matrix): array of integer; + + /// Возвращает исходные метки классов в порядке внутреннего кодирования. + function GetClassLabels: array of string; + end; + + IClassifierInternal = interface procedure SetClassLabels(classes: array of string); end; @@ -172,4 +175,4 @@ type implementation -end. \ No newline at end of file +end. diff --git a/bin/Lib/MLDatasets.pas b/bin/Lib/MLDatasets.pas index 0732901b8..52518e8f8 100644 --- a/bin/Lib/MLDatasets.pas +++ b/bin/Lib/MLDatasets.pas @@ -51,7 +51,7 @@ type function StratifiedTrainTestSplit(testRatio: real := 0.2; seed: integer := -1): (Dataset, Dataset); /// Возвращает первые n строк таблицы данных. - function Head(n: integer := 5): DataFrame; + function Head(n: integer := 10): DataFrame; /// Возвращает краткое описание датасета (метаданные). function Describe: DataFrame; @@ -260,6 +260,9 @@ type /// Датасет российских городов (задача кластеризации) static function RussianCities: Dataset; + + /// Датасет пассажиров Титаника (задача классификации) + static function TitanicRu: Dataset; {/// Датасет результатов экзамена студентов (классификация) static function StudentExam: Dataset; @@ -411,6 +414,9 @@ end; function Dataset.TrainTestSplit(testRatio: real; shuffle: boolean; seed: integer): (Dataset, Dataset); begin + if Data = nil then + ArgumentNullError(ER_ARG_NULL, 'Data'); + var (trainDf, testDf) := Data.TrainTestSplit(testRatio, shuffle, seed); var trainDs := CloneMeta(trainDf); @@ -423,96 +429,15 @@ function Dataset.StratifiedTrainTestSplit(testRatio: real; seed: integer): (Data begin if Data = nil then ArgumentNullError(ER_ARG_NULL, 'Data'); - - if (testRatio <= 0.0) or (testRatio >= 1.0) then - ArgumentError(ER_TEST_RATIO_INVALID, testRatio); - + if Task <> Classification then Error(ER_STRATIFIED_ONLY_FOR_CLASSIFICATION); - var n := Data.RowCount; - if n < 2 then - ArgumentError(ER_EMPTY_DATA, 'StratifiedTrainTestSplit'); - - var actualSeed := if seed >= 0 then seed else System.Environment.TickCount and integer.MaxValue; - var rnd := new System.Random(actualSeed); - - // --- target column - var ci := Data.ColumnIndex(Target); - var col := Data.GetColumn(ci); - - var groups := new Dictionary>; - - // --- группировка по target - case col.Info.ColType of - - ctInt: - begin - var data := IntColumn(col).Data; - - for var i := 0 to n - 1 do - begin - var key: object := data[i]; - - var lst: List; - if not groups.TryGetValue(key, lst) then - begin - lst := new List; - groups[key] := lst; - end; - - lst.Add(i); - end; - end; - - ctStr: - begin - var data := StrColumn(col).Data; - - for var i := 0 to n - 1 do - begin - var key: object := data[i]; - - var lst: List; - if not groups.TryGetValue(key, lst) then - begin - lst := new List; - groups[key] := lst; - end; - - lst.Add(i); - end; - end; - - else - Error(ER_GROUPBY_UNSUPPORTED_KEY_TYPE, col.Info.ColType); - end; - - var trainIdx := new List; - var testIdx := new List; - - // --- split внутри каждой группы - foreach var kvp in groups do - begin - var arr := kvp.Value.ToArray; - arr.Shuffle(rnd); - - var m := arr.Length; - var rawSize := Round(m * testRatio); - var testSize := rawSize.Clamp(1, m - 1); - - for var i := 0 to testSize - 1 do - testIdx.Add(arr[i]); - - for var i := testSize to m - 1 do - trainIdx.Add(arr[i]); - end; - - var trainDf := Data.TakeRows(trainIdx.ToArray); - var testDf := Data.TakeRows(testIdx.ToArray); + var (trainDf, testDf) := + Data.StratifiedTrainTestSplit(Target, testRatio, seed); var trainDs := CloneMeta(trainDf); - var testDs := CloneMeta(testDf); + var testDs := CloneMeta(testDf); Result := (trainDs, testDs); end; @@ -1361,6 +1286,20 @@ begin Result := ds; end; +static function Datasets.TitanicRu: Dataset; +begin + var ds := Load('titanic_ru'); + + ds.Data := ds.Data.SetCategorical([ + 'Выжил', + 'Класс', + 'Пол', + 'ПортПосадки' + ]); + + Result := ds; +end; + {static function Datasets.StudentExam: Dataset; begin NotImplementedError(ER_NOT_IMPLEMENTED, 'Datasets.StudentExam'); @@ -1437,4 +1376,4 @@ begin .ToArray; end; -end. \ No newline at end of file +end. diff --git a/bin/Lib/MLModelsABC.pas b/bin/Lib/MLModelsABC.pas index e778324d2..1e67ab066 100644 --- a/bin/Lib/MLModelsABC.pas +++ b/bin/Lib/MLModelsABC.pas @@ -304,7 +304,7 @@ type /// для двух классов — частный случай softmax. /// Оптимизация выполняется по кросс-энтропийной функции потерь /// с поддержкой L2-регуляризации - LogisticRegression = class(IProbabilisticClassifier) + LogisticRegression = class(IProbabilisticClassifier, IClassifierInternal) private fW: Matrix; // p x k fIntercept: Vector; // k @@ -427,6 +427,21 @@ type Result.Threshold := threshold; end; end; + + RegSplitResult = record + Found: boolean; + Feature: integer; + Threshold: real; + WeightedScore: real; + + static function Invalid: RegSplitResult; + begin + Result.Found := false; + Result.Feature := -1; + Result.Threshold := 0.0; + Result.WeightedScore := real.PositiveInfinity; + end; + end; /// Интерфейс критерия разбиения узла дерева. /// Определяет функцию нечистоты (impurity), которая используется для оценки качества разбиения @@ -523,7 +538,7 @@ type function BuildNode(X: Matrix; y: Vector; indices: array of integer; depth: integer): DecisionTreeNode; - function FindBestSplit(X: Matrix; y: Vector; indices: array of integer; + function FindBestSplitCore(X: Matrix; y: Vector; indices: array of integer; var bestF: integer; var bestT: real): boolean; function MajorityClass(y: Vector; indices: array of integer): integer; @@ -534,84 +549,6 @@ type function Clone: DecisionTreeCore; end; -//============================ -// DecisionTreeBase -//============================ -/// Базовый абстрактный класс дерева решений. -/// Используется только DecisionTreeRegressor -/// Classifier использует DecisionTreeCore -/// Реализует общую логику построения структуры дерева: -/// рекурсивное разбиение, контроль глубины, -/// минимального числа объектов и расчет важности признаков. -/// Конкретная логика вычисления значения листа и критерия разбиения задается в наследнике - DecisionTreeRegressorBase = abstract class(ITreeModel) - protected - fRoot: DecisionTreeNode; - fMaxDepth: integer; - fMinSamplesSplit: integer; - fMinSamplesLeaf: integer; - fFitted: boolean; - fCriterion: ISplitCriterion; - fFeatureImportances: Vector; - fRandomSeed: integer; - fMaxFeatures: integer := 0; - fRowIndices: array of integer := nil; - - fRng: System.Random; - fUserProvidedSeed: boolean; - - function BuildTree(X: Matrix; y: Vector; indices: array of integer; depth: integer): DecisionTreeNode; - - //function FindBestSplit0(X: Matrix; y: Vector; indices: array of integer): SplitResult; - - function FindBestSplit(X: Matrix; y: Vector; indices: array of integer): SplitResult; virtual; abstract; - function IsPure(y: Vector; indices: array of integer): boolean; virtual; -// -------------------------- - - function LeafValue(y: Vector; indices: array of integer): real; virtual; abstract; - function LeafNode(value: real): DecisionTreeNode; - procedure CopyBaseState(dest: DecisionTreeRegressorBase); - function GetFeatureSubset(nFeatures: integer): array of integer; virtual; - - procedure SetRowIndices(rows: array of integer); - public -/// Создает дерево решений: -/// • maxDepth — максимальная глубина дерева. -/// • minSamplesSplit — минимальное число объектов для разбиения узла. -/// • minSamplesLeaf — минимальное число объектов в листе - constructor Create(maxDepth: integer; minSamplesSplit: integer; minSamplesLeaf: integer; - criterion: ISplitCriterion; seed: integer); - -/// Возвращает вектор важности признаков. -/// Важность вычисляется как суммарное уменьшение -/// нечистоты (impurity reduction) по всем разбиениям. -/// Значения нормированы так, что сумма равна 1. - function FeatureImportances: Vector; - -/// Обучает дерево решений для задачи регрессии. -/// X — матрица m × n (m объектов, n признаков). -/// y — вектор длины m с непрерывными значениями. -/// -/// Примечание: -/// • обученное состояние дерева НЕ копируется методом Clone - function Fit(X: Matrix; y: Vector): ISupervisedModel; virtual; abstract; - -/// Выполняет предсказание для матрицы X. -/// Возвращает вектор прогнозов. -/// Для регрессии — вещественные значения. -/// Для классификации — метки классов. - function Predict(X: Matrix): Vector; virtual; abstract; - -/// Копирует только конфигурацию модели (без обученного состояния). -/// Используется для создания независимых экземпляров модели. - function Clone: IModel; virtual; abstract; - -/// Возвращает true, если дерево обучено. -/// Если false — Predict вызовет ошибку. - property IsFitted: boolean read fFitted; - - function Name: string := Self.GetType.Name; - end; //============================ // DecisionTreeClassifier @@ -619,7 +556,7 @@ type /// Дерево решений для задачи классификации. /// Использует критерий нечистоты (обычно Gini) для выбора оптимальных разбиений. /// В листьях хранится наиболее частый класс - DecisionTreeClassifier = class(IClassifier) + DecisionTreeClassifier = class(IClassifier, IClassifierInternal) private fMaxDepth: integer; fMinSamplesSplit: integer; @@ -687,6 +624,78 @@ type function Clone: IModel; end; +//============================ +// DecisionTreeRegressorBase +//============================ +/// Базовый абстрактный класс дерева решений. +/// Используется только DecisionTreeRegressor +/// Classifier использует DecisionTreeCore +/// Реализует общую логику построения структуры дерева: +/// рекурсивное разбиение, контроль глубины, +/// минимального числа объектов и расчет важности признаков. +/// Конкретная логика вычисления значения листа и критерия разбиения задается в наследнике + DecisionTreeRegressorBase = abstract class(ITreeModel) + protected + fRoot: DecisionTreeNode; + fMaxDepth: integer; + fMinSamplesSplit: integer; + fMinSamplesLeaf: integer; + fFitted: boolean; + fCriterion: ISplitCriterion; + fFeatureImportances: Vector; + fRandomSeed: integer; + fMaxFeatures: integer := 0; + fRowIndices: array of integer := nil; + + fRng: System.Random; + fUserProvidedSeed: boolean; + + function IsPure(y: Vector; indices: array of integer): boolean; virtual; +// -------------------------- + + function LeafValue(y: Vector; indices: array of integer): real; virtual; abstract; + function LeafNode(value: real): DecisionTreeNode; + + procedure SetRowIndices(rows: array of integer); + public +/// Создает дерево решений: +/// • maxDepth — максимальная глубина дерева. +/// • minSamplesSplit — минимальное число объектов для разбиения узла. +/// • minSamplesLeaf — минимальное число объектов в листе + constructor Create(maxDepth: integer; minSamplesSplit: integer; minSamplesLeaf: integer; + criterion: ISplitCriterion; seed: integer); + +/// Возвращает вектор важности признаков. +/// Важность вычисляется как суммарное уменьшение +/// нечистоты (impurity reduction) по всем разбиениям. +/// Значения нормированы так, что сумма равна 1. + function FeatureImportances: Vector; + +/// Обучает дерево решений для задачи регрессии. +/// X — матрица m × n (m объектов, n признаков). +/// y — вектор длины m с непрерывными значениями. +/// +/// Примечание: +/// • обученное состояние дерева НЕ копируется методом Clone + function Fit(X: Matrix; y: Vector): ISupervisedModel; virtual; abstract; + +/// Выполняет предсказание для матрицы X. +/// Возвращает вектор прогнозов. +/// Для регрессии — вещественные значения. +/// Для классификации — метки классов. + function Predict(X: Matrix): Vector; virtual; abstract; + +/// Копирует только конфигурацию модели (без обученного состояния). +/// Используется для создания независимых экземпляров модели. + function Clone: IModel; virtual; abstract; + +/// Возвращает true, если дерево обучено. +/// Если false — Predict вызовет ошибку. + property IsFitted: boolean read fFitted; + + function Name: string := Self.GetType.Name; + end; + //============================ // DecisionTreeRegressor //============================ @@ -698,6 +707,11 @@ type DecisionTreeRegressor = class(DecisionTreeRegressorBase, IRegressor) private fLeafL2: real; + fSortedOrders: array of array of integer; + fSortedValues: array of array of real; + + fVisitMarks: array of integer; + fVisitId: integer; function PredictOne(X: Matrix; rowIndex: integer): real; @@ -706,12 +720,19 @@ type /// В регрессии это среднее целевой переменной /// с учетом L2-регуляризации (если leafL2 > 0). function LeafValue(y: Vector; indices: array of integer): real; override; + function BuildTreeNew(X: Matrix; y: Vector; indices: array of integer; depth: integer): DecisionTreeNode; + function BuildTreeNode(X: Matrix; y: Vector; nodeOrders: array of array of integer; depth: integer): DecisionTreeNode; - function FindBestSplitReg(X: Matrix; y: Vector; indices: array of integer): SplitResult; + function FindBestSplitReg(X: Matrix; y: Vector; nodeOrders: array of array of integer): RegSplitResult; + procedure BuildSortedOrders(X: Matrix; indices: array of integer); + function BuildInitialNodeOrders(indices: array of integer): array of array of integer; + procedure SplitNodeOrders(X: Matrix; nodeOrders: array of array of integer; feature: integer; threshold: real; + var leftOrders, rightOrders: array of array of integer); + function BuildMembershipMask(rowCount: integer; indices: array of integer): array of boolean; + procedure ComputeNodeStats(yData: array of real; indices: array of integer; var sumAll, sumSqAll: real); + function WeightedVariance(n, leftCount: integer; leftSum, leftSumSq, sumAll, sumSqAll: real): real; + function GetFeatureSubset(p: integer): array of integer; -/// Ищет лучшее разбиение узла по всем признакам и возможным порогам. -/// Критерий — максимальное уменьшение дисперсии. - function FindBestSplit(X: Matrix; y: Vector; indices: array of integer): SplitResult; override; /// Проверяет, является ли узел "чистым". /// Для регрессии это означает, что все значения y одинаковы /// или разбиение больше не имеет смысла. @@ -909,7 +930,7 @@ type /// Строит ансамбль классификационных деревьев, обученных на /// bootstrap-подвыборках объектов и случайных подмножествах признаков. /// Итоговое предсказание формируется голосованием деревьев или агрегацией вероятностей классов - RandomForestClassifier = class(RandomForestBase, IProbabilisticClassifier) + RandomForestClassifier = class(RandomForestBase, IProbabilisticClassifier, IClassifierInternal) private fTrees: array of DecisionTreeCore; fIndexToClass: array of integer; @@ -1178,7 +1199,7 @@ type /// • validation early stopping /// • OOB early stopping /// • staged prediction - GradientBoostingClassifier = class(IProbabilisticClassifier) + GradientBoostingClassifier = class(IProbabilisticClassifier, IClassifierInternal) private // hyperparams fNEstimators: integer; @@ -1398,7 +1419,7 @@ type /// Реализует вероятностные предсказания через PredictProba /// ВАЖНО: KNN чувствителен к масштабу признаков. /// Всегда используйте StandardScaler или MinMaxScaler в Pipeline перед KNN. - KNNClassifier = class(KNNBase, IProbabilisticClassifier) + KNNClassifier = class(KNNBase, IProbabilisticClassifier, IClassifierInternal) private // ==== classification state ==== fYEnc: array of integer; @@ -2334,8 +2355,8 @@ const 'leafL2 должно быть >= 0 ({0}).!!' + 'leafL2 must be >= 0 ({0}).'; ER_MIN_LEAF_GT_SPLIT = - 'minSamplesSplit ({1}) должен быть > 2 * minSamplesLeaf ({0}).!!' + - 'minSamplesSplit ({1}) must be > 2 * minSamplesLeaf ({0}).'; + 'minSamplesSplit ({1}) должен быть >= 2 * minSamplesLeaf ({0}).!!' + + 'minSamplesSplit ({1}) must be >= 2 * minSamplesLeaf ({0}).'; ER_OOB_NOT_ENABLED = 'OOB score не включен для этой модели. Установите computeOOB = true в конструкторе.!!' + 'OOB score is not enabled for this model. Set computeOOB = true in the constructor.'; @@ -2960,6 +2981,10 @@ begin fClassToIndex[unique[i]] := i; fIndexToClass[i] := unique[i]; end; + + SetLength(fClassLabels, fIndexToClass.Length); + for var i := 0 to fIndexToClass.Length - 1 do + fClassLabels[i] := fIndexToClass[i].ToString; // --- init fW := new Matrix(p, fClassCount); @@ -3356,26 +3381,22 @@ begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); - var P := PredictProba(X); - - var m := P.RowCount; - Result := new Vector(m); - - for var i := 0 to m - 1 do - begin - var internalIdx := P.RowArgMax(i); - Result[i] := fIndexToClass[internalIdx]; - end; + var labels := PredictLabels(X); + + Result := new Vector(labels.Length); + + for var i := 0 to labels.Length - 1 do + Result[i] := fIndexToClass[labels[i]]; end; function LogisticRegression.PredictLabels(X: Matrix): array of integer; begin - var v := Predict(X); + var P := PredictProba(X); - Result := new integer[v.Length]; + SetLength(Result, P.RowCount); - for var i := 0 to v.Length - 1 do - Result[i] := Round(v[i]); + for var i := 0 to P.RowCount - 1 do + Result[i] := P.RowArgMax(i); end; function LogisticRegression.ToString: string; @@ -3707,7 +3728,7 @@ begin var bestFeature: integer; var bestThreshold: real; - if not FindBestSplit(X, y, indices, bestFeature, bestThreshold) then + if not FindBestSplitCore(X, y, indices, bestFeature, bestThreshold) then exit(CreateLeaf(y, indices)); // 4. split → только индексы @@ -3757,7 +3778,7 @@ begin end; // Новая реализация (O(n log n · p)) -function DecisionTreeCore.FindBestSplit( +function DecisionTreeCore.FindBestSplitCore( X: Matrix; y: Vector; indices: array of integer; @@ -3963,62 +3984,6 @@ begin fRng := new System.Random(fRandomSeed); end; -procedure DecisionTreeRegressorBase.CopyBaseState(dest: DecisionTreeRegressorBase); -begin - dest.fMaxDepth := fMaxDepth; - dest.fMinSamplesSplit := fMinSamplesSplit; - dest.fMinSamplesLeaf := fMinSamplesLeaf; - dest.fFitted := fFitted; - dest.fRandomSeed := fRandomSeed; - dest.fMaxFeatures := fMaxFeatures; - - dest.fUserProvidedSeed := fUserProvidedSeed; - - if fUserProvidedSeed then - dest.fRng := new System.Random(fRandomSeed) - else - dest.fRng := new System.Random; - - // MUST be stateless - if fCriterion <> nil then - dest.fCriterion := fCriterion; - - if fFeatureImportances <> nil then - dest.fFeatureImportances := fFeatureImportances.Clone; - - if fRoot <> nil then - dest.fRoot := fRoot.Clone; - - if fRowIndices <> nil then - dest.fRowIndices := Copy(fRowIndices); -end; - -function DecisionTreeRegressorBase.GetFeatureSubset(nFeatures: integer): array of integer; -begin - if (fMaxFeatures = 0) or (fMaxFeatures >= nFeatures) then - begin - Result := new integer[nFeatures]; - for var i := 0 to nFeatures-1 do - Result[i] := i; - exit; - end; - - var all := new List; - for var i := 0 to nFeatures-1 do - all.Add(i); - - var subset := new integer[fMaxFeatures]; - - for var k := 0 to fMaxFeatures-1 do - begin - var idx := fRng.Next(all.Count); - subset[k] := all[idx]; - all.RemoveAt(idx); - end; - - Result := subset; -end; - procedure DecisionTreeRegressorBase.SetRowIndices(rows: array of integer); begin if Length(rows) = 0 then @@ -4046,54 +4011,52 @@ begin Result := n; end; -function DecisionTreeRegressorBase.BuildTree(X: Matrix; y: Vector; +function DecisionTreeRegressor.BuildTreeNew(X: Matrix; y: Vector; indices: array of integer; depth: integer): DecisionTreeNode; begin + var nodeOrders := BuildInitialNodeOrders(indices); + Result := BuildTreeNode(X, y, nodeOrders, depth); +end; + +function DecisionTreeRegressor.BuildTreeNode(X: Matrix; y: Vector; + nodeOrders: array of array of integer; depth: integer): DecisionTreeNode; +begin + var indices := nodeOrders[0]; + var n := indices.Length; + if (fMaxDepth >= 0) and (depth >= fMaxDepth) then exit(LeafNode(LeafValue(y, indices))); - if indices.Length < fMinSamplesSplit then + if n < fMinSamplesSplit then exit(LeafNode(LeafValue(y, indices))); - if IsPure(y, indices) then + var yData := y.Data; + var sumAll, sumSqAll: real; + ComputeNodeStats(yData, indices, sumAll, sumSqAll); + + var meanAll := sumAll / n; + var parentVar := (sumSqAll / n) - meanAll * meanAll; + if parentVar < 0 then + parentVar := 0.0; + + if parentVar < 1e-12 then exit(LeafNode(LeafValue(y, indices))); - var parentImp := fCriterion.Impurity(y, indices); - - if double.IsNaN(parentImp) or double.IsInfinity(parentImp) then - exit(LeafNode(LeafValue(y, indices))); - - var split := FindBestSplit(X, y, indices); + var split := FindBestSplitReg(X, y, nodeOrders); if not split.Found then exit(LeafNode(LeafValue(y, indices))); - var left := new List; - var right := new List; + var leftOrders, rightOrders: array of array of integer; + SplitNodeOrders(X, nodeOrders, split.Feature, split.Threshold, leftOrders, rightOrders); + var leftArr := leftOrders[0]; + var rightArr := rightOrders[0]; - foreach var i in indices do - if X[i, split.Feature] <= split.Threshold then - left.Add(i) - else - right.Add(i); - - if (left.Count < fMinSamplesLeaf) or - (right.Count < fMinSamplesLeaf) then + if (leftArr.Length < fMinSamplesLeaf) or + (rightArr.Length < fMinSamplesLeaf) then exit(LeafNode(LeafValue(y, indices))); - var leftArr := left.ToArray; - var rightArr := right.ToArray; - - var leftImp := fCriterion.Impurity(y, leftArr); - var rightImp := fCriterion.Impurity(y, rightArr); - - var n := indices.Length; - - var weighted := - (real(leftArr.Length) / n) * leftImp + - (real(rightArr.Length) / n) * rightImp; - - var delta := parentImp - weighted; + var delta := parentVar - split.WeightedScore; if double.IsNaN(delta) or double.IsInfinity(delta) then exit(LeafNode(LeafValue(y, indices))); @@ -4101,14 +4064,13 @@ begin if delta < 0 then delta := 0.0; - // КЛЮЧЕВОЙ PRODUCTION-ФИЛЬТР if delta <= 0 then exit(LeafNode(LeafValue(y, indices))); fFeatureImportances[split.Feature] += delta; - var leftNode := BuildTree(X, y, leftArr, depth + 1); - var rightNode := BuildTree(X, y, rightArr, depth + 1); + var leftNode := BuildTreeNode(X, y, leftOrders, depth + 1); + var rightNode := BuildTreeNode(X, y, rightOrders, depth + 1); var node := new DecisionTreeNode; node.IsLeaf := false; @@ -4127,123 +4089,295 @@ begin Result := fCriterion.Impurity(y, indices) < EPS; end; -function DecisionTreeRegressor.FindBestSplitReg(X: Matrix; y: Vector; indices: array of integer): SplitResult; +function DecisionTreeRegressor.FindBestSplitReg(X: Matrix; y: Vector; + nodeOrders: array of array of integer): RegSplitResult; begin + Result := RegSplitResult.Invalid; + + var indices := nodeOrders[0]; + var n := indices.Length; + if n < 2 then + exit; + + var xData := X.Data; + var yData := y.Data; + + var sumAll, sumSqAll: real; + ComputeNodeStats(yData, indices, sumAll, sumSqAll); + var bestScore := real.PositiveInfinity; var bestFeature := -1; var bestThreshold := 0.0; - var n := indices.Length; - if n < 2 then + var features := GetFeatureSubset(X.ColCount); + + for var fj := 0 to features.Length - 1 do begin - Result.Found := false; - exit; - end; - - // --- parent sums - var sumAll := 0.0; - var sumSqAll := 0.0; - - for var i := 0 to n-1 do - begin - var v := y[indices[i]]; - sumAll += v; - sumSqAll += v*v; - end; - - var ProcessFeature: integer -> () := j -> - begin - var pairs: array of (real, integer); - SetLength(pairs, n); - - for var i := 0 to n-1 do - begin - var idx := indices[i]; - pairs[i] := (X[idx,j], idx); - end; - - pairs.Sort(p -> p.Item1); + var j := features[fj]; + var order := nodeOrders[j]; + var orderLen := order.Length; var leftCount := 0; var leftSum := 0.0; var leftSumSq := 0.0; - for var i := 1 to n-1 do + var prevValue := 0.0; + var firstIncluded := true; + + for var i := 0 to orderLen - 1 do begin - var v := y[pairs[i-1].Item2]; + var idx := order[i]; + var xCur := xData[idx, j]; + var yCur := yData[idx]; + + if not firstIncluded then + begin + var rightCount := n - leftCount; + + if (leftCount >= fMinSamplesLeaf) and + (rightCount >= fMinSamplesLeaf) and + (prevValue <> xCur) then + begin + var rightCountReal := rightCount; + var leftMean := leftSum / leftCount; + var leftVar := leftSumSq / leftCount - leftMean * leftMean; + + var rightSum := sumAll - leftSum; + var rightSumSq := sumSqAll - leftSumSq; + var rightMean := rightSum / rightCount; + var rightVar := rightSumSq / rightCount - rightMean * rightMean; + + if leftVar < 0 then + leftVar := 0.0; + if rightVar < 0 then + rightVar := 0.0; + + var weighted := (leftCount * leftVar + rightCountReal * rightVar) / n; + + if weighted < bestScore then + begin + bestScore := weighted; + bestFeature := j; + bestThreshold := (prevValue + xCur) * 0.5; + end; + end; + end; leftCount += 1; - leftSum += v; - leftSumSq += v*v; - - var rightCount := n - leftCount; - - if (leftCount < fMinSamplesLeaf) or - (rightCount < fMinSamplesLeaf) then - continue; - - var x1 := pairs[i-1].Item1; - var x2 := pairs[i].Item1; - - if x1 = x2 then - continue; - - var rightSum := sumAll - leftSum; - var rightSumSq := sumSqAll - leftSumSq; - - var leftMean := leftSum / leftCount; - var rightMean := rightSum / rightCount; - - var leftVar := (leftSumSq / leftCount) - leftMean*leftMean; - var rightVar := (rightSumSq / rightCount) - rightMean*rightMean; - - if leftVar < 0 then leftVar := 0.0; - if rightVar < 0 then rightVar := 0.0; - - var weighted := - (real(leftCount) / n) * leftVar + - (real(rightCount) / n) * rightVar; - - if double.IsNaN(weighted) or double.IsInfinity(weighted) then - continue; - - if weighted < bestScore then - begin - bestScore := weighted; - bestFeature := j; - bestThreshold := (x1 + x2) * 0.5; - end; + leftSum += yCur; + leftSumSq += yCur * yCur; + prevValue := xCur; + firstIncluded := false; end; end; - var p := X.ColCount; - - if (fMaxFeatures <= 0) or (fMaxFeatures >= p) then - begin - for var j := 0 to p-1 do - ProcessFeature(j); - end - else - begin - var feat := new integer[p]; - for var i := 0 to p-1 do - feat[i] := i; - - for var i := 0 to fMaxFeatures-1 do - begin - var r := i + fRng.Next(p - i); - var tmp := feat[i]; - feat[i] := feat[r]; - feat[r] := tmp; - end; - - for var k := 0 to fMaxFeatures-1 do - ProcessFeature(feat[k]); - end; - Result.Found := bestFeature <> -1; Result.Feature := bestFeature; Result.Threshold := bestThreshold; + Result.WeightedScore := bestScore; +end; + +type + SortPair = record + Value: real; + Index: integer; + end; + SortPairComparer = class(IComparer) + public + function Compare(a, b: SortPair): integer; + begin + Result := a.Value.CompareTo(b.Value); + end; + end; + +procedure DecisionTreeRegressor.BuildSortedOrders(X: Matrix; indices: array of integer); +begin + var p := X.ColCount; + var n := indices.Length; + var xData := X.Data; + + SetLength(fSortedOrders, p); + SetLength(fSortedValues, p); + + for var j := 0 to p - 1 do + begin + var pairs: array of SortPair; + SetLength(pairs, n); + + for var i := 0 to n - 1 do + begin + var idx := indices[i]; + pairs[i].Value := xData[idx, j]; + pairs[i].Index := idx; + end; + + System.Array.Sort(pairs,new SortPairComparer); + + fSortedOrders[j] := new integer[n]; + fSortedValues[j] := new real[n]; + + for var i := 0 to n - 1 do + begin + fSortedValues[j][i] := pairs[i].Value; + fSortedOrders[j][i] := pairs[i].Index; + end; + end; +end; + +function DecisionTreeRegressor.BuildInitialNodeOrders(indices: array of integer): array of array of integer; +begin + var maxIdx := -1; + for var i := 0 to indices.Length - 1 do + if indices[i] > maxIdx then + maxIdx := indices[i]; + + var inNode := BuildMembershipMask(maxIdx + 1, indices); + SetLength(Result, Length(fSortedOrders)); + + for var j := 0 to Length(fSortedOrders) - 1 do + begin + var cnt := 0; + foreach var idx in fSortedOrders[j] do + if inNode[idx] then + cnt += 1; + + Result[j] := new integer[cnt]; + var k := 0; + foreach var idx in fSortedOrders[j] do + if inNode[idx] then + begin + Result[j][k] := idx; + k += 1; + end; + end; +end; + +procedure DecisionTreeRegressor.SplitNodeOrders(X: Matrix; + nodeOrders: array of array of integer; + feature: integer; + threshold: real; + var leftOrders, rightOrders: array of array of integer); +begin + var xData := X.Data; + var p := Length(nodeOrders); + + fVisitId += 1; + var mark := fVisitId; + + var splitArr := nodeOrders[feature]; + var splitLen := splitArr.Length; + + var leftCount := 0; + + for var i := 0 to splitLen - 1 do + begin + var idx := splitArr[i]; + + if xData[idx, feature] <= threshold then + begin + fVisitMarks[idx] := mark; + leftCount += 1; + end; + end; + + var rightCount := splitLen - leftCount; + + SetLength(leftOrders, p); + SetLength(rightOrders, p); + + for var j := 0 to p - 1 do + begin + var src := nodeOrders[j]; + var n := src.Length; + + var left := new integer[leftCount]; + var right := new integer[rightCount]; + + var li := 0; + var ri := 0; + + for var k := 0 to n - 1 do + begin + var idx := src[k]; + + if fVisitMarks[idx] = mark then + begin + left[li] := idx; + li += 1; + end + else + begin + right[ri] := idx; + ri += 1; + end; + end; + + leftOrders[j] := left; + rightOrders[j] := right; + end; +end; + +function DecisionTreeRegressor.BuildMembershipMask(rowCount: integer; indices: array of integer): array of boolean; +begin + Result := new boolean[rowCount]; + for var i := 0 to indices.Length - 1 do + Result[indices[i]] := true; +end; + +procedure DecisionTreeRegressor.ComputeNodeStats(yData: array of real; indices: array of integer; var sumAll, sumSqAll: real); +begin + sumAll := 0.0; + sumSqAll := 0.0; + + for var i := 0 to indices.Length - 1 do + begin + var v := yData[indices[i]]; + sumAll += v; + sumSqAll += v * v; + end; +end; + +function DecisionTreeRegressor.WeightedVariance(n, leftCount: integer; leftSum, leftSumSq, sumAll, sumSqAll: real): real; +begin + var rightCount := n - leftCount; + var rightSum := sumAll - leftSum; + var rightSumSq := sumSqAll - leftSumSq; + + var leftMean := leftSum / leftCount; + var rightMean := rightSum / rightCount; + + var leftVar := (leftSumSq / leftCount) - leftMean * leftMean; + var rightVar := (rightSumSq / rightCount) - rightMean * rightMean; + + if leftVar < 0 then leftVar := 0.0; + if rightVar < 0 then rightVar := 0.0; + + Result := + (real(leftCount) / n) * leftVar + + (real(rightCount) / n) * rightVar; +end; + +function DecisionTreeRegressor.GetFeatureSubset(p: integer): array of integer; +begin + if (fMaxFeatures <= 0) or (fMaxFeatures >= p) then + begin + Result := Arr(0..p - 1); + exit; + end; + + Result := new integer[fMaxFeatures]; + var feat := new integer[p]; + for var i := 0 to p - 1 do + feat[i] := i; + + for var i := 0 to fMaxFeatures - 1 do + begin + var r := i + fRng.Next(p - i); + var tmp := feat[i]; + feat[i] := feat[r]; + feat[r] := tmp; + Result[i] := feat[i]; + end; end; //============================== @@ -4320,6 +4454,9 @@ begin fCriterion := new GiniCriterion(classes.Length); fIndexToClass := classes; + SetLength(fClassLabels, fIndexToClass.Length); + for var i := 0 to fIndexToClass.Length - 1 do + fClassLabels[i] := fIndexToClass[i].ToString; // --- encoded vector var yEncoded := new Vector(yEncArr); @@ -4330,6 +4467,7 @@ begin fMinSamplesSplit, fMinSamplesLeaf, fCriterion, + classes.Length, fMaxFeatures, fRandomSeed ); @@ -4341,6 +4479,16 @@ begin end; function DecisionTreeClassifier.Predict(X: Matrix): Vector; +begin + var labels := PredictLabels(X); + + Result := new Vector(labels.Length); + + for var i := 0 to labels.Length - 1 do + Result[i] := fIndexToClass[labels[i]]; +end; + +function DecisionTreeClassifier.PredictLabels(X: Matrix): array of integer; begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); @@ -4353,27 +4501,13 @@ begin if X.ColCount <> fFeatureImportances.Length then DimensionError(ER_FEATURE_COUNT_MISMATCH, X.ColCount, fFeatureImportances.Length); - + var predIdx := fCore.Predict(X); - - var n := predIdx.Length; - Result := new Vector(n); - - for var i := 0 to n - 1 do - begin - var k := Round(predIdx[i]); - Result[i] := fIndexToClass[k]; - end; -end; - -function DecisionTreeClassifier.PredictLabels(X: Matrix): array of integer; -begin - var v := Predict(X); - Result := new integer[v.Length]; + Result := new integer[predIdx.Length]; - for var i := 0 to v.Length - 1 do - Result[i] := Round(v[i]); + for var i := 0 to predIdx.Length - 1 do + Result[i] := Round(predIdx[i]); end; function DecisionTreeClassifier.Clone: IModel; @@ -4465,11 +4599,6 @@ begin Result := value; end; -function DecisionTreeRegressor.FindBestSplit(X: Matrix; y: Vector; indices: array of integer): SplitResult; -begin - Result := FindBestSplitReg(X, y, indices); -end; - function DecisionTreeRegressor.IsPure(y: Vector; indices: array of integer): boolean; begin var first := y[indices[0]]; @@ -4508,7 +4637,12 @@ begin if indices = nil then indices := Arr(0..X.RowCount - 1); - fRoot := BuildTree(X, y, indices, 0); + BuildSortedOrders(X, indices); + + SetLength(fVisitMarks, X.RowCount); + fVisitId := 0; + + fRoot := BuildTreeNew(X, y, indices, 0); var s := fFeatureImportances.Sum; if s > 0 then @@ -4518,6 +4652,10 @@ begin fFitted := true; fRowIndices := nil; + fSortedOrders := nil; + fSortedValues := nil; + + fVisitMarks := nil; Result := Self; end; @@ -4914,6 +5052,10 @@ begin var yEncArr := EncodeLabelsInt(yInt, fIndexToClass); fClassCount := fIndexToClass.Length; + + SetLength(fClassLabels, fIndexToClass.Length); + for var i := 0 to fIndexToClass.Length - 1 do + fClassLabels[i] := fIndexToClass[i].ToString; if fClassCount < 2 then ArgumentError(ER_NEED_AT_LEAST_TWO_CLASSES); @@ -5016,6 +5158,16 @@ begin end; function RandomForestClassifier.Predict(X: Matrix): Vector; +begin + var labels := PredictLabels(X); + + Result := new Vector(labels.Length); + + for var i := 0 to labels.Length - 1 do + Result[i] := fIndexToClass[labels[i]]; +end; + +function RandomForestClassifier.PredictLabels(X: Matrix): array of integer; begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); @@ -5028,7 +5180,7 @@ begin if X.ColCount <> fFeatureCount then DimensionError(ER_FEATURE_COUNT_MISMATCH, X.ColCount, fFeatureCount); - + var n := X.RowCount; var treeCount := fTrees.Length; @@ -5038,19 +5190,17 @@ begin if fClassCount <= 0 then Error(ER_MODEL_NOT_INITIALIZED); - var resultVec := new Vector(n); + Result := new integer[n]; var counts := new integer[fClassCount]; - + for var i := 0 to n - 1 do begin - // обнуление счётчиков for var c := 0 to fClassCount - 1 do counts[c] := 0; - // голосование деревьев for var t := 0 to treeCount - 1 do begin - var cls := fTrees[t].PredictRow(X, i); // <-- КЛЮЧЕВОЕ изменение + var cls := fTrees[t].PredictRow(X, i); if (cls < 0) or (cls >= fClassCount) then ArgumentError(ER_LABEL_INDEX_INVALID); @@ -5058,7 +5208,6 @@ begin counts[cls] += 1; end; - // выбор класса var bestClass := 0; var bestCount := counts[0]; @@ -5069,21 +5218,8 @@ begin bestClass := c; end; - // декодирование - resultVec[i] := fIndexToClass[bestClass]; + Result[i] := bestClass; end; - - Result := resultVec; -end; - -function RandomForestClassifier.PredictLabels(X: Matrix): array of integer; -begin - var v := Predict(X); - - Result := new integer[v.Length]; - - for var i := 0 to v.Length - 1 do - Result[i] := Round(v[i]); end; function RandomForestClassifier.PredictProba(X: Matrix): Matrix; @@ -6049,6 +6185,10 @@ begin var yEncoded := EncodeLabelsInt(yTrainInt, fClasses); fClassCount := fClasses.Length; + + SetLength(fClassLabels, fClasses.Length); + for var i := 0 to fClasses.Length - 1 do + fClassLabels[i] := fClasses[i].ToString; if fClassCount < 2 then ArgumentError(ER_NEED_AT_LEAST_TWO_CLASSES); @@ -6793,38 +6933,22 @@ begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); - var probs := PredictProba(X); - - var nSamples := X.RowCount; - var classCount := fClassCount; - - Result := new Vector(nSamples); - - for var i := 0 to nSamples - 1 do - begin - var bestClassIndex := 0; - var bestValue := probs[i, 0]; - - for var cls := 1 to classCount - 1 do - if probs[i, cls] > bestValue then - begin - bestValue := probs[i, cls]; - bestClassIndex := cls; - end; - - // возвращаем оригинальную метку - Result[i] := fClasses[bestClassIndex]; - end; + var labels := PredictLabels(X); + + Result := new Vector(labels.Length); + + for var i := 0 to labels.Length - 1 do + Result[i] := fClasses[labels[i]]; end; function GradientBoostingClassifier.PredictLabels(X: Matrix): array of integer; begin - var v := Predict(X); + var probs := PredictProba(X); - Result := new integer[v.Length]; + SetLength(Result, probs.RowCount); - for var i := 0 to v.Length - 1 do - Result[i] := Round(v[i]); + for var i := 0 to probs.RowCount - 1 do + Result[i] := probs.RowArgMax(i); end; procedure GradientBoostingClassifier.SetClassLabels(classes: array of string); @@ -7088,6 +7212,10 @@ begin SetLength(fClasses, fClassCount); for var i := 0 to fClassCount - 1 do fClasses[i] := classesInt[i]; + + SetLength(fClassLabels, fClassCount); + for var i := 0 to fClassCount - 1 do + fClassLabels[i] := fClasses[i].ToString; // сохранить encoded y SetLength(fYEnc, n); @@ -7121,6 +7249,16 @@ begin end; function KNNClassifier.Predict(X: Matrix): Vector; +begin + var labels := PredictLabels(X); + + Result := new Vector(labels.Length); + + for var i := 0 to labels.Length - 1 do + Result[i] := fClasses[labels[i]]; +end; + +function KNNClassifier.PredictLabels(X: Matrix): array of integer; begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); @@ -7137,17 +7275,10 @@ begin var trainRows := fXTrain.Data.Rows; var testRows := X.Data.Rows; - Result := new Vector(m); + Result := new integer[m]; for var i := 0 to m - 1 do begin - // заполнить расстояния - // for var t := 0 to n - 1 do - // begin - // fNeighbors[t].dist := SquaredL2(t, X, i); - // fNeighbors[t].idx := t; - // end; - var rowTest := testRows[i]; for var t := 0 to n - 1 do begin @@ -7165,10 +7296,8 @@ begin fNeighbors[t].idx := t; end; - // выбрать k ближайших QuickSelect(fK - 1); - // exact match: если среди k ближайших есть dist=0, возвращаем его класс var exactCls := -1; for var t := 0 to fK - 1 do if fNeighbors[t].dist < KNN_EPS then @@ -7179,11 +7308,10 @@ begin if exactCls <> -1 then begin - Result[i] := fClasses[exactCls]; + Result[i] := exactCls; continue; end; - // voting (stamping) fEpoch += 1; var touchCount := 0; @@ -7207,7 +7335,6 @@ begin end else begin - // weighted: веса 1 / dist (dist = squared distance) for var t := 0 to fK - 1 do begin var trainIdx := fNeighbors[t].idx; @@ -7224,7 +7351,6 @@ begin if dist < KNN_EPS then begin - // считаем как exact match exactCls := cls; break; end @@ -7238,11 +7364,10 @@ begin if exactCls <> -1 then begin - Result[i] := fClasses[exactCls]; + Result[i] := exactCls; continue; end; - // argmax только по touched var bestCls := fTouched[0]; var bestVotes := fVotes[bestCls]; @@ -7258,20 +7383,10 @@ begin end; end; - Result[i] := fClasses[bestCls]; + Result[i] := bestCls; end; end; -function KNNClassifier.PredictLabels(X: Matrix): array of integer; -begin - var v := Predict(X); - - Result := new integer[v.Length]; - - for var i := 0 to v.Length - 1 do - Result[i] := Round(v[i]); -end; - function KNNClassifier.PredictProba(X: Matrix): Matrix; begin if not fFitted then @@ -7281,15 +7396,29 @@ begin var m := X.RowCount; var n := fXTrain.RowCount; + var p := fXTrain.ColCount; + + var trainRows := fXTrain.Data.Rows; + var testRows := X.Data.Rows; Result := new Matrix(m, fClassCount); // предполагаем нулевую инициализацию for var i := 0 to m - 1 do begin // заполнить расстояния + var rowTest := testRows[i]; for var t := 0 to n - 1 do begin - fNeighbors[t].dist := SquaredL2(t, X, i); + var rowTrain := trainRows[t]; + var sum := 0.0; + + for var j := 0 to p - 1 do + begin + var diff := rowTrain[j] - rowTest[j]; + sum += diff * diff; + end; + + fNeighbors[t].dist := sum; fNeighbors[t].idx := t; end; diff --git a/bin/Lib/MLPipelineABC.pas b/bin/Lib/MLPipelineABC.pas index 543448ea5..cc7c68435 100644 --- a/bin/Lib/MLPipelineABC.pas +++ b/bin/Lib/MLPipelineABC.pas @@ -115,18 +115,32 @@ type function Transform(df: DataFrame): DataFrame; /// Делает предсказание модели для объектов из DataFrame. + /// Для задач классификации возвращает внутренние индексы классов (0..K-1). + /// В отличие от обычных моделей-классификаторов, здесь Predict + /// возвращает не исходные метки классов, а их внутренние индексы. /// Доступен после обучения конвейера (Fit). function Predict(df: DataFrame): Vector; + /// Возвращает исходные строковые метки классов для объектов из DataFrame. + /// Это удобная расшифровка результата Predict через GetClassLabels. + /// Доступен только для задач классификации после Fit. + function PredictLabels(df: DataFrame): array of string; + /// Возвращает матрицу вероятностей (nSamples × nClasses). /// Доступен только если конечная модель поддерживает IProbabilisticClassifier. function PredictProba(df: DataFrame): Matrix; - /// Возвращает метки классов в порядке кодирования (0,1,2,...), - /// используемом при EncodeLabels. - /// Доступен только для задач классификации после Fit. + /// Возвращает исходные метки классов в порядке внутреннего кодирования (0..K-1), + /// соответствующем индексам, возвращаемым Predict. + /// Доступен только для задач классификации. function GetClassLabels: array of string; + /// Возвращает внутренние индексы истинных меток классов для DataFrame + /// в соответствии с кодированием, полученным при Fit. + /// Используется для вычисления метрик после Predict. + /// Доступен только для задач классификации после Fit. + function GetEncodedLabels(df: DataFrame): Vector; + function ToString: string; override; function Name: string := Self.GetType.Name; @@ -221,6 +235,7 @@ implementation uses MLExceptions; uses DataAdapters; +uses MLUtilsABC; const ER_PIPELINE_MODIFY_AFTER_FIT = @@ -282,12 +297,16 @@ const 'Операция доступна только для задач классификации!!Operation is only available for classification tasks'; ER_CLASSES_NOT_AVAILABLE = 'Метки классов недоступны. Убедитесь, что конвейер обучен и задача — классификация!!Class labels are not available. Ensure the pipeline is fitted and the task is classification'; + ER_LABEL_INDEX_OUT_OF_RANGE = + 'Индекс метки {0} вне диапазона [0, {1})!!Label index {0} is out of range [0, {1})'; ER_LABELENCODER_TARGET_NOT_ALLOWED = 'LabelEncoder нельзя применять к целевой переменной — кодирование выполняется внутри модели!!LabelEncoder cannot be applied to target — encoding is handled internally by the model'; ER_ENCODELABELS_NOT_CATEGORICAL = 'Целевой столбец должен быть категориальным для задач классификации!!Target column must be categorical for classification tasks'; ER_REGRESSION_TARGET_MUST_BE_NUMERIC = 'Целевой столбец "{0}" должен быть числовым для задач регрессии!!Target column "{0}" must be numeric for regression tasks'; + ER_PREPROCESSOR_ROWCOUNT_CHANGED = + 'DataFrame-преобразователь не должен изменять число строк!!DataFrame preprocessor must preserve RowCount'; ER_PIPELINE_TARGET_TRANSFORM_NOT_ALLOWED = 'Преобразование целевой переменной "{0}" запрещено в DataPipeline!!' + 'Transformation of target variable "{0}" is not allowed in DataPipeline'; @@ -403,8 +422,11 @@ begin // --- DataFrame steps for var i := 0 to fDataSteps.Count - 1 do begin + var prevRows := current.RowCount; fDataSteps[i] := fDataSteps[i].Fit(current); current := fDataSteps[i].Transform(current); + if current.RowCount <> prevRows then + Error(ER_PREPROCESSOR_ROWCOUNT_CHANGED); end; fFinalFeatures := ResolveFinalFeatures(current); @@ -421,7 +443,12 @@ begin var current := df; foreach var s in fDataSteps do + begin + var prevRows := current.RowCount; current := s.Transform(current); + if current.RowCount <> prevRows then + Error(ER_PREPROCESSOR_ROWCOUNT_CHANGED); + end; Result := current; end; @@ -631,7 +658,7 @@ begin if fTask = tkClassification then begin - if fModel is IClassifier(var cls) then + if fModel is IClassifierInternal(var cls) then cls.SetClassLabels(classes) else Error(ER_MODEL_NOT_CLASSIFIER); @@ -643,6 +670,7 @@ end; function DataPipeline.Transform(df: DataFrame): DataFrame := TransformDataFrame(df); + function DataPipeline.Predict(df: DataFrame): Vector; begin if df = nil then @@ -664,6 +692,28 @@ begin Result := (fModel as IPredictiveModel).Predict(X); end; +function DataPipeline.PredictLabels(df: DataFrame): array of string; +begin + if not fFitted then + NotFittedError(ER_FIT_NOT_CALLED); + + if fTask <> tkClassification then + ArgumentError(ER_NOT_CLASSIFICATION); + + var encoded := LabelsToInts(Predict(df)); + var classes := GetClassLabels; + Result := new string[encoded.Length]; + + for var i := 0 to encoded.Length - 1 do + begin + var idx := encoded[i]; + if (idx < 0) or (idx >= classes.Length) then + Error(ER_LABEL_INDEX_OUT_OF_RANGE, idx, classes.Length); + + Result[i] := classes[idx]; + end; +end; + function DataPipeline.PredictProba(df: DataFrame): Matrix; begin if df = nil then @@ -685,6 +735,25 @@ begin Result := (fModel as IProbabilisticClassifier).PredictProba(X); end; +function DataPipeline.GetEncodedLabels(df: DataFrame): Vector; +begin + if not fFitted then + NotFittedError(ER_FIT_NOT_CALLED); + + if fTask <> tkClassification then + ArgumentError(ER_NOT_CLASSIFICATION); + + if df = nil then + ArgumentNullError(ER_ARG_NULL, 'df'); + + if not df.HasColumn(fTarget) then + ArgumentError(ER_COLUMN_NOT_FOUND, fTarget); + + var classes := GetClassLabels; + var labels := df.TransformLabels(fTarget, classes); + Result := new Vector(labels); +end; + function DataPipeline.GetClassLabels: array of string; begin if not fFitted then @@ -929,8 +998,11 @@ begin // --- 1) DataFrame шаги for var i := 0 to fDataSteps.Count - 1 do begin + var prevRows := current.RowCount; fDataSteps[i] := fDataSteps[i].Fit(current); current := fDataSteps[i].Transform(current); + if current.RowCount <> prevRows then + Error(ER_PREPROCESSOR_ROWCOUNT_CHANGED); end; // --- 2) вычислить финальные признаки diff --git a/bin/Lib/MetricsABC.pas b/bin/Lib/MetricsABC.pas index dff74ceb6..ce124e679 100644 --- a/bin/Lib/MetricsABC.pas +++ b/bin/Lib/MetricsABC.pas @@ -1028,8 +1028,21 @@ begin var correct := 0; for var i := 0 to n - 1 do - if Round(yTrue[i]) = Round(yPred[i]) then + begin + var yt := yTrue[i]; + var yp := yPred[i]; + var ytInt := Round(yt); + var ypInt := Round(yp); + + if Abs(yt - ytInt) > 1e-12 then + ArgumentError(ER_INVALID_CLASS_LABEL, yt); + + if Abs(yp - ypInt) > 1e-12 then + ArgumentError(ER_INVALID_CLASS_LABEL, yp); + + if ytInt = ypInt then correct += 1; + end; Result := correct / n; end; @@ -1078,12 +1091,13 @@ begin for var i := 0 to n - 1 do begin var yt := data[i]; + var ytInt := Round(yt); // проверка: метки должны быть целыми - if Abs(yt - integer(yt)) > 1e-12 then + if Abs(yt - ytInt) > 1e-12 then ArgumentError(ER_INVALID_CLASS_LABEL, yt); - if integer(yt) = yPred[i] then + if ytInt = yPred[i] then correct += 1; end; @@ -1944,8 +1958,8 @@ begin if double.IsNaN(yp) or double.IsInfinity(yp) then ArgumentError(ER_INVALID_VALUE, 'yPred', i); - var ytInt := integer(yt); - var ypInt := integer(yp); + var ytInt := Round(yt); + var ypInt := Round(yp); if Abs(yt - ytInt) > 1e-12 then ArgumentError(ER_INVALID_CLASS_LABEL, yt); diff --git a/bin/Lib/PlotML.pas b/bin/Lib/PlotML.pas index 967ed2fa2..efc741ba0 100644 --- a/bin/Lib/PlotML.pas +++ b/bin/Lib/PlotML.pas @@ -20,6 +20,7 @@ uses System, System.Windows.Shapes, System.Threading, System.Windows.Threading, + System.Text, InteractiveDataDisplay.WPF, LinearAlgebraML; @@ -110,6 +111,7 @@ type procedure Surface(x1, x2: array of real; nx, ny: integer; f: Matrix -> array of integer; pal: PlotML.Palette := nil); procedure Heatmap(m: Matrix); + procedure HeatCell(value, minValue, maxValue: real; text: string := nil); procedure Text(s: string; x: real := 0.5; y: real := 0.5); @@ -151,11 +153,14 @@ type static procedure DrawPoints(chart: ChartWPF; x, y: array of real; color: ColorWPF; size: real; marker: MarkerType; legend: string); - static procedure DrawHeatmap(chart: ChartWPF; m: array[,] of real); + static procedure DrawHeatmap(chart: ChartWPF; m: array[,] of real; names: array of string := nil); static procedure DrawHist(chart: ChartWPF; x: array of real; bins: integer; color: ColorWPF; alpha: real; legend: string); + static procedure DrawHistMany(chart: ChartWPF; arrays: array of array of real; + bins: integer; colors: array of ColorWPF; alpha: real; legends: array of string); + static procedure DrawSurface(chart: ChartWPF; labels: array of integer; nx, ny: integer; xmin, xmax, ymin, ymax: real; pal: Palette); @@ -177,9 +182,13 @@ type static procedure Hist(x: array of real; bins: integer := 0; color: ColorWPF := DefaultColor; alpha: real := 0.7; legend: string := nil); + static procedure HistMany(arrays: array of array of real; bins: integer := 0; + colors: array of ColorWPF := nil; alpha: real := 0.7; legend: array of string := nil); + static procedure PairPlot(X: array[,] of real; labels: array of integer; names: array of string); static procedure Heatmap(m: array[,] of real); + static procedure Heatmap(m: array[,] of real; names: array of string); static procedure Surface(labels: array of integer; nx, ny: integer; xmin, xmax, ymin, ymax: real; pal: PlotML.Palette := nil); @@ -208,6 +217,7 @@ type := PairPlot(X.Data, labels, names); static procedure Heatmap(m: Matrix) := Heatmap(m.Data); + static procedure Heatmap(m: Matrix; names: array of string) := Heatmap(m.Data, names); static function Grid(rows,cols: integer): Figure; @@ -226,6 +236,8 @@ type static procedure Clear; static procedure Save(filename: string); + + static function DebugVisualTree: string; static property Title: string write SetTitle; end; @@ -238,6 +250,8 @@ type fBinsCount: integer; fDescription: string; fMaxCount: integer; + MinValue: real := real.NaN; + MaxValue: real := real.NaN; public constructor Create; @@ -250,6 +264,24 @@ type property Description: string read fDescription write fDescription; property MaxCount: integer read fMaxCount; end; + + HeatmapPlot = class(PlotWPF) + private + fCells: List := new List; + fMinValue: real; + fMaxValue: real; + + function LerpColor(c1, c2: ColorWPF; t: real): ColorWPF; + function Clamp01(x: real): real; + function ColorForValue(v: real): ColorWPF; + public + constructor Create; + + procedure SetData(m: array[,] of real); + + property MinValue: real read fMinValue; + property MaxValue: real read fMaxValue; + end; SurfacePlot = class(PlotWPF) private @@ -377,6 +409,69 @@ begin Result := c; end; +function Clamp01(x: real): real; +begin + if x < 0 then + Result := 0 + else if x > 1 then + Result := 1 + else + Result := x; +end; + +function LerpColor(c1, c2: ColorWPF; t: real): ColorWPF; +begin + t := Clamp01(t); + + Result := Color.FromRgb( + byte(Round(c1.R + (c2.R - c1.R) * t)), + byte(Round(c1.G + (c2.G - c1.G) * t)), + byte(Round(c1.B + (c2.B - c1.B) * t)) + ); +end; + +function HeatmapColor(v, minValue, maxValue: real): ColorWPF; +begin + if real.IsNaN(v) or real.IsInfinity(v) then + exit(Color.FromRgb(180, 180, 180)); + + if minValue = maxValue then + exit(Color.FromRgb(255, 255, 255)); + + var blue := Color.FromRgb(49, 130, 189); + var white := Color.FromRgb(255, 255, 255); + var red := Color.FromRgb(222, 45, 38); + + if (minValue < 0) and (maxValue > 0) then + begin + if v < 0 then + Result := LerpColor(white, blue, Abs(v / minValue)) + else + Result := LerpColor(white, red, v / maxValue); + exit; + end; + + Result := LerpColor(blue, red, (v - minValue) / (maxValue - minValue)); +end; + +procedure DumpVisualNode(sb: StringBuilder; obj: DependencyObject; level: integer); +begin + if obj = nil then + exit; + + var fe := obj as FrameworkElement; + var name := if (fe <> nil) and (fe.Name <> nil) and (fe.Name <> '') then fe.Name else '-'; + + sb.Append(''.PadLeft(level * 2)); + sb.Append(obj.GetType.FullName); + sb.Append(' Name='); + sb.AppendLine(name); + + var cnt := VisualTreeHelper.GetChildrenCount(obj); + for var i := 0 to cnt - 1 do + DumpVisualNode(sb, VisualTreeHelper.GetChild(obj, i), level + 1); +end; + function MakeHistogram(data: array of real; bins: integer): (array of real, array of real); begin var xmin := data.Min; @@ -631,6 +726,35 @@ begin end); end; +procedure Cell.HeatCell(value, minValue, maxValue: real; text: string); +begin + Plot.RunUI(() -> + begin + EnsureChart; + + var container := chart.Content as GridWPF; + if container = nil then + begin + container := new GridWPF; + chart.Content := container; + end; + + container.Children.Clear; + container.Background := new SolidColorBrush(HeatmapColor(value, minValue, maxValue)); + + if text <> nil then + begin + var tb := new System.Windows.Controls.TextBlock; + tb.Text := text; + tb.FontSize := 14; + tb.FontWeight := System.Windows.FontWeights.Bold; + tb.HorizontalAlignment := System.Windows.HorizontalAlignment.Center; + tb.VerticalAlignment := System.Windows.VerticalAlignment.Center; + container.Children.Add(tb); + end; + end); +end; + procedure Cell.Hist(x: array of real; bins: integer; color: ColorWPF; alpha: real; legend: string); begin Plot.RunUI(() -> @@ -811,6 +935,25 @@ begin end); end; +static function Plot.DebugVisualTree: string; +begin + var sb := new StringBuilder; + + RunUI(() -> + begin + if win = nil then + begin + sb.AppendLine('win = nil'); + exit; + end; + + sb.AppendLine('Window content tree:'); + DumpVisualNode(sb, win.Content as DependencyObject, 0); + end); + + Result := sb.ToString; +end; + static function Plot.CreateLineSeries(x,y: array of real; c: Color): LineGraphWPF; begin var g := new LineGraphWPF; @@ -870,7 +1013,13 @@ begin tb.HorizontalAlignment := System.Windows.HorizontalAlignment.Center; tb.VerticalAlignment := System.Windows.VerticalAlignment.Center; - var grid := chart.Parent as System.Windows.Controls.Grid; + var grid := chart.Content as GridWPF; + if grid = nil then + begin + grid := new GridWPF; + chart.Content := grid; + end; + if grid <> nil then begin tb.HorizontalAlignment := System.Windows.HorizontalAlignment.Center; @@ -893,19 +1042,85 @@ begin AddSeries(chart, g); end; -static procedure Plot.DrawHeatmap(chart: ChartWPF; m: array[,] of real); +static procedure Plot.DrawHeatmap(chart: ChartWPF; m: array[,] of real; names: array of string); begin var rows := m.GetLength(0); var cols := m.GetLength(1); - var x := ArrGen(cols, i -> i); - var y := ArrGen(rows, i -> i); + var container := new GridWPF; + chart.Content := container; - var g := new HeatmapGraph; - - g.Plot(m, x, y); + var topOffset := if names <> nil then 1 else 0; + var leftOffset := if names <> nil then 1 else 0; - AddSeries(chart, g); + for var i := 0 to rows + topOffset - 1 do + container.RowDefinitions.Add(new RowDefinition); + + for var j := 0 to cols + leftOffset - 1 do + container.ColumnDefinitions.Add(new ColumnDefinition); + + if names <> nil then + begin + for var j := 0 to cols - 1 do + begin + var tb := new System.Windows.Controls.TextBlock; + tb.Text := names[j]; + tb.FontSize := 13; + tb.FontWeight := System.Windows.FontWeights.Normal; + tb.TextAlignment := TextAlignment.Center; + tb.TextWrapping := TextWrapping.Wrap; + tb.Margin := new Thickness(6); + tb.HorizontalAlignment := HorizontalAlignment.Center; + tb.VerticalAlignment := VerticalAlignment.Center; + GridWPF.SetRow(tb, 0); + GridWPF.SetColumn(tb, j + 1); + container.Children.Add(tb); + end; + + for var i := 0 to rows - 1 do + begin + var tb := new System.Windows.Controls.TextBlock; + tb.Text := names[i]; + tb.FontSize := 13; + tb.FontWeight := System.Windows.FontWeights.Normal; + tb.TextAlignment := TextAlignment.Center; + tb.TextWrapping := TextWrapping.Wrap; + tb.Margin := new Thickness(6); + tb.HorizontalAlignment := HorizontalAlignment.Center; + tb.VerticalAlignment := VerticalAlignment.Center; + GridWPF.SetRow(tb, i + 1); + GridWPF.SetColumn(tb, 0); + container.Children.Add(tb); + end; + end; + + var valuesOnly := new HeatmapPlot; + valuesOnly.SetData(m); + var minValue := valuesOnly.MinValue; + var maxValue := valuesOnly.MaxValue; + + for var i := 0 to rows - 1 do + for var j := 0 to cols - 1 do + begin + var border := new Border; + border.BorderBrush := Brushes.White; + border.BorderThickness := new Thickness(0.25); + border.Background := new SolidColorBrush(HeatmapColor(m[i, j], minValue, maxValue)); + + var tb := new System.Windows.Controls.TextBlock; + tb.Text := $'{m[i, j]:F2}'; + tb.FontSize := 15; + tb.FontWeight := System.Windows.FontWeights.SemiBold; + tb.TextAlignment := TextAlignment.Center; + tb.HorizontalAlignment := HorizontalAlignment.Center; + tb.VerticalAlignment := VerticalAlignment.Center; + + border.Child := tb; + + GridWPF.SetRow(border, i + topOffset); + GridWPF.SetColumn(border, j + leftOffset); + container.Children.Add(border); + end; end; class procedure Plot.AddSeries(chart: ChartWPF; series: UIElement); @@ -977,7 +1192,15 @@ static procedure Plot.Heatmap(m: array[,] of real); begin RunUI(() -> begin - DrawHeatmap(rootChart, m); + DrawHeatmap(rootChart, m, nil); + end); +end; + +static procedure Plot.Heatmap(m: array[,] of real; names: array of string); +begin + RunUI(() -> + begin + DrawHeatmap(rootChart, m, names); end); end; @@ -1183,6 +1406,94 @@ begin chart.PlotHeight := hist.MaxCount * 1.1; end; +static procedure Plot.HistMany(arrays: array of array of real; bins: integer; + colors: array of ColorWPF; alpha: real; legend: array of string); +begin + RunUI(() -> + begin + Plot.DrawHistMany(rootChart, arrays, bins, colors, alpha, legend); + end); +end; + +static procedure Plot.DrawHistMany(chart: ChartWPF; arrays: array of array of real; + bins: integer; colors: array of ColorWPF; alpha: real; legends: array of string); +begin + EnsureAxes(chart); + + var hasLegend := (legends <> nil) and (legends.Length > 0); + + if hasLegend then + chart.LegendVisibility := Visibility.Visible; + + if (arrays = nil) or (arrays.Length = 0) then exit; + + // --- общий диапазон --- + var globalMin := real.MaxValue; + var globalMax := real.MinValue; + + foreach var arr in arrays do + begin + if (arr = nil) or (arr.Length = 0) then continue; + globalMin := Min(globalMin, arr.Min); + globalMax := Max(globalMax, arr.Max); + end; + + if globalMin = real.MaxValue then + exit; + + if globalMax <= globalMin then + exit; + + if bins = 0 then + begin + var maxLen := 0; + + foreach var arr in arrays do + if (arr <> nil) and (arr.Length > maxLen) then + maxLen := arr.Length; + + if maxLen = 0 then + exit; + + bins := Round(Sqrt(maxLen)); + end; + + var maxCount := 0; + + // --- рисуем --- + for var k := 0 to arrays.Length - 1 do + begin + var x := arrays[k]; + if (x = nil) or (x.Length = 0) then continue; + + var hist := new HistogramPlot; + + hist.BinsCount := bins; + hist.Color := if (colors<>nil) and (k < colors.Length) then colors[k] else NextRootColor; + hist.Alpha := alpha; + + // фиксируем диапазон + hist.MinValue := globalMin; + hist.MaxValue := globalMax; + + hist.SetData(x); + + if hist.MaxCount > maxCount then + maxCount := hist.MaxCount; + + if hasLegend and (k < legends.Length) then + hist.Description := legends[k]; + + AddSeries(chart, hist); + end; + + // --- оси --- + chart.PlotOriginX := Floor(globalMin); + chart.PlotWidth := Ceil(globalMax) - Floor(globalMin); + chart.PlotOriginY := 0; + chart.PlotHeight := maxCount * 1.1; +end; + constructor HistogramPlot.Create; begin fBins := new List; @@ -1200,8 +1511,8 @@ begin if x=nil then exit; if x.Length=0 then exit; - var xmin := Floor(x.Min); - var xmax := Ceil(x.Max); + var xmin := if not real.IsNaN(MinValue) then Floor(MinValue) else Floor(x.Min); + var xmax := if not real.IsNaN(MaxValue) then Ceil(MaxValue) else Ceil(x.Max); if xmax=xmin then exit; @@ -1245,10 +1556,118 @@ begin fBins.Add(poly); Children.Add(poly); end; - + fMaxCount := counts.Max; end; +constructor HeatmapPlot.Create; +begin + IsAutoFitEnabled := false; + fMinValue := 0; + fMaxValue := 0; +end; + +function HeatmapPlot.Clamp01(x: real): real; +begin + if x < 0 then + Result := 0 + else if x > 1 then + Result := 1 + else + Result := x; +end; + +function HeatmapPlot.LerpColor(c1, c2: ColorWPF; t: real): ColorWPF; +begin + t := Clamp01(t); + + Result := Color.FromRgb( + byte(Round(c1.R + (c2.R - c1.R) * t)), + byte(Round(c1.G + (c2.G - c1.G) * t)), + byte(Round(c1.B + (c2.B - c1.B) * t)) + ); +end; + +function HeatmapPlot.ColorForValue(v: real): ColorWPF; +begin + Result := HeatmapColor(v, fMinValue, fMaxValue); +end; + +procedure HeatmapPlot.SetData(m: array[,] of real); +begin + Children.Clear; + fCells.Clear; + + if m = nil then + exit; + + var rows := m.GetLength(0); + var cols := m.GetLength(1); + + if (rows = 0) or (cols = 0) then + exit; + + var foundFinite := false; + fMinValue := 0; + fMaxValue := 0; + + for var i := 0 to rows - 1 do + for var j := 0 to cols - 1 do + begin + var v := m[i, j]; + if real.IsNaN(v) or real.IsInfinity(v) then + continue; + + if not foundFinite then + begin + fMinValue := v; + fMaxValue := v; + foundFinite := true; + end + else + begin + if v < fMinValue then + fMinValue := v; + if v > fMaxValue then + fMaxValue := v; + end; + end; + + if not foundFinite then + exit; + + for var i := 0 to rows - 1 do + for var j := 0 to cols - 1 do + begin + var v := m[i, j]; + var brush := new SolidColorBrush(ColorForValue(v)); + + var x0 := j; + var x1 := j + 1; + + // Делаем нулевую строку верхней строкой матрицы. + var y0 := rows - i - 1; + var y1 := rows - i; + + var poly := new Polygon; + var pts := new PointCollection; + + pts.Add(new Point(x0, y0)); + pts.Add(new Point(x0, y1)); + pts.Add(new Point(x1, y1)); + pts.Add(new Point(x1, y0)); + + PlotWPF.SetPoints(poly, pts); + + poly.Fill := brush; + poly.Stroke := Brushes.LightGray; + poly.StrokeThickness := 0.5; + + fCells.Add(poly); + Children.Add(poly); + end; +end; + procedure SurfacePlot.SetData(labels: array of integer; nx, ny: integer; xmin, xmax, ymin, ymax: real; pal: Palette); begin @@ -1374,4 +1793,4 @@ end; initialization InitUI; -end. \ No newline at end of file +end. diff --git a/bin/Lib/PreprocessorABC.pas b/bin/Lib/PreprocessorABC.pas index be15d6f60..b9e624cc1 100644 --- a/bin/Lib/PreprocessorABC.pas +++ b/bin/Lib/PreprocessorABC.pas @@ -21,6 +21,7 @@ unit PreprocessorABC; interface uses DataFrameABC; +uses DataFrameABCCore; uses System; uses MLCoreABC; @@ -122,9 +123,9 @@ type ImputeStrategy = (isMean, isConstant, isMedian); -/// Заполняет пропущенные значения (NA) в числовых столбцах -/// Поддерживает стратегии isMean и isConstant -/// Работает только с Int и Float столбцами +/// Заполняет пропущенные значения (NA) в столбцах DataFrame +/// Стратегии isMean и isMedian работают только с числовыми столбцами +/// Стратегия isConstant работает с любыми поддерживаемыми типами Imputer = class(IPreprocessor, IColumnsBoundStep) private cols: array of string; @@ -133,13 +134,14 @@ type means: array of real; medians: array of real; fitted: boolean; + function BuildImputedColumn(df: DataFrame; idx, imputerIndex: integer): Column; public /// Создаёт Imputer с заполнением средним значением - constructor Create(params columns: array of string); + constructor Create(columns: array of string); /// Создаёт Imputer с заданной стратегией заполнения - constructor Create(strategy: ImputeStrategy; params columns: array of string); + constructor Create(strategy: ImputeStrategy; columns: array of string); /// Создаёт Imputer с константной стратегией заполнения - constructor Create(value: object; params columns: array of string); + constructor Create(value: object; columns: array of string); /// Вычисляет значения для заполнения пропусков. /// df — таблица данных. @@ -216,6 +218,8 @@ const 'Стратегия импутации {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 = @@ -313,7 +317,15 @@ begin else res.AddIntColumn(col, data, valid); - Result := res.SetCategorical([col]); + 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 LabelEncoder.FitTransform(df: DataFrame): DataFrame; @@ -331,6 +343,24 @@ function LabelEncoder.Clone: IPreprocessor; begin Result := new LabelEncoder(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 //----------------------------- @@ -413,42 +443,56 @@ begin if srcIdx < 0 then ArgumentError(ER_COLUMN_NOT_FOUND, col); + var rowCount := df.RowCount; var catCount := categories.Length; - - var res := df; - - // === Генерация one-hot столбцов === + 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 - var catIdx := j; - var newName := col + '_' + categories[j]; - - var Encode: DataFrameCursor -> integer := c -> - begin - if not c.IsValid(col) then - begin - Result := 0; - exit; - end; - - var s := c.Str(col); - - var idx: integer; - if not indexByValue.TryGetValue(s, idx) then - begin - Result := 0; - exit; - end; - - Result := Ord(idx = catIdx); - end; - - res := res.AddDerivedIntColumn(newName, Encode); + dataCols.Add(new integer[rowCount]); + validCols.Add([True] * rowCount); end; - - // === удаление исходного столбца === - res := res.Drop([srcIdx]); - + + 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; @@ -487,7 +531,7 @@ end; // Imputer //----------------------------- -constructor Imputer.Create(strategy: ImputeStrategy; params columns: array of string); +constructor Imputer.Create(strategy: ImputeStrategy; columns: array of string); begin if (columns = nil) or (columns.Length = 0) then ArgumentError(ER_IMPUTER_NO_COLUMNS); @@ -498,12 +542,12 @@ begin fitted := false; end; -constructor Imputer.Create(params columns: array of string); +constructor Imputer.Create(columns: array of string); begin Create(ImputeStrategy.isMean, columns); end; -constructor Imputer.Create(value: object; params columns: array of string); +constructor Imputer.Create(value: object; columns: array of string); begin if (columns = nil) or (columns.Length = 0) then ArgumentError(ER_IMPUTER_NO_COLUMNS); @@ -600,6 +644,179 @@ begin 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 @@ -610,90 +827,39 @@ begin ((constants = nil) or (constants.Length <> cols.Length)) then Error(ER_IMPUTER_CONSTANTS_INVALID); - var res := df; - - for var i := 0 to cols.Length - 1 do + 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 := cols[i]; - - // --- ВАЖНО: используем актуальную схему - var idx := res.Schema.IndexOf(name); - var ct := res.Schema.ColumnTypeAt(idx); - - if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then - Error(ER_IMPUTER_COLUMN_NOT_NUMERIC, name); - - var capturedIdx := idx; + var name := df.Schema.NameAt(i); + var imputerIndex := cols.IndexOf(name); - case strategy of - isMean: - begin - var m := means[i]; - res := res.ReplaceColumnFloat( - name, - c -> (if c.IsValid(capturedIdx) then c.Float(capturedIdx) else m) - ); - end; - - isConstant: - begin - var v := constants[i]; - 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); - - res := res.ReplaceColumnInt( - name, - c -> (if c.IsValid(capturedIdx) then c.Int(capturedIdx) else k) - ); - end - else // ctFloat - begin - var r: real; - try - r := real(v); - except - on e: Exception do - Error(ER_IMPUTER_CONSTANT_TYPE_MISMATCH, name); - end; - - res := res.ReplaceColumnFloat( - name, - c -> (if c.IsValid(capturedIdx) then c.Float(capturedIdx) else r) - ); - end; - end; - - isMedian: - begin - var m := medians[i]; - res := res.ReplaceColumnFloat( - name, - c -> (if c.IsValid(capturedIdx) then c.Float(capturedIdx) else m) - ); - end; - - else - Error(ER_IMPUTER_STRATEGY_NOT_SUPPORTED, strategy); + 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;