Привет, Хабр!
PL/R — это процедурный язык для PostgreSQL, который позволяет писать функции на языке R. Когда хочется использовать все богатство статистических пакетов и алгоритмов R, не покидая привычного SQL‑окружения, PL/R приходит на помощь. Плюс ко всему: нет необходимости создавать отдельные сервисы для анализа, что уменьшает задержки и упрощает архитектуру приложения.
Создаем расширение:
CREATE EXTENSION IF NOT EXISTS plr;
Основы создания функций на PL/R: от простого к сложному
Базовый пример
Начнем с самого простого примера, который поможет понять синтаксис PL/R и основы работы с аргументами.
CREATE OR REPLACE FUNCTION add_numbers(a double precision, b double precision) RETURNS double precision AS $$ # Просто складываем два числа – прям как на пальцах return(a + b) $$ LANGUAGE plr;
Запустим её:
SELECT add_numbers(2.5, 3.5); -- Результат: 6.0
Так можно вызывать R‑код из SQL.
Работа с массивами
PL/R позволяет передавать массивы данных и обрабатывать их как в обычном R. Пример функции, которая принимает массив чисел и возвращает массив их квадратов:
CREATE OR REPLACE FUNCTION square_elements(numbers numeric[]) RETURNS numeric[] AS $$ # Преобразуем входной массив в вектор R num_vector <- as.numeric(numbers) # Вычисляем квадрат каждого элемента squared <- num_vector^2 # Возвращаем результат обратно в виде массива return(squared) $$ LANGUAGE plr;
Запускаем функцию:
SELECT square_elements(ARRAY[1, 2, 3, 4, 5]) AS squared_values;
Увидим: {1, 4, 9, 16, 25}.
Возврат таблиц
В PL/R можно возвращать не только скаляры или массивы, но и целые таблицы. Представьте ситуацию, когда у вас есть два массива — с названиями товаров и их ценами. Функция должна вернуть таблицу с названием, ценой и рассчитанной скидкой (скажем, 10% от цены).
CREATE OR REPLACE FUNCTION product_discounts(names text[], prices numeric[]) RETURNS TABLE(product_name text, price numeric, discount numeric) AS $$ # Преобразуем входные массивы в векторы R name_vec <- as.character(names) price_vec <- as.numeric(prices) # Вычисляем скидку в 10% discount_vec <- price_vec * 0.10 # Формируем результирующий data.frame result <- data.frame(product_name = name_vec, price = price_vec, discount = discount_vec) return(result) $$ LANGUAGE plr;
Вызов функции:
SELECT * FROM product_discounts( ARRAY['Товар A', 'Товар B', 'Товар C'], ARRAY[100.0, 250.0, 400.0] );
Результатом станет таблица с тремя колонками, где для каждого продукта указана цена и вычисленная скидка.
Валидация входных данных и обработка ошибок
Ни в коем случае не забывайте про проверки — некорректные данные могут привести к катастрофе. Например, если массив пустой, стоит выбросить ошибку. Функция, вычисляющая среднее значение с проверкой:
CREATE OR REPLACE FUNCTION safe_mean(numbers numeric[]) RETURNS numeric AS $$ # Если массив пуст или равен NULL, генерируем ошибку if (is.null(numbers) || length(numbers) == 0) { plr.error("Пустой массив чисел: вычисление среднего невозможно!") } num_vector <- as.numeric(numbers) mean_value <- mean(num_vector) return(mean_value) $$ LANGUAGE plr;
При вызове с пустым массивом PostgreSQL вернет ошибку.
Логирование и отладка
Когда функция начинает вести себя не так, как ожидается, полезно добавить логирование. PL/R позволяет выводить сообщения в логи PostgreSQL через plr.notice(). Пример функции с логированием:
CREATE OR REPLACE FUNCTION debug_add_numbers(a double precision, b double precision) RETURNS double precision AS $$ # Логируем входные параметры plr.notice("Запуск debug_add_numbers: a = " %+% a %+% ", b = " %+% b) result <- a + b # Логируем результат plr.notice("Результат сложения: " %+% result) return(result) $$ LANGUAGE plr;
Запустив эту функцию, увидите сообщения в логах.
Преобразование типов и динамическая обработка данных
Иногда данные приходят не в том формате, который нужен. Например, массив дат может прийти в виде строк, и их надо преобразовать в формат Date. Рассмотрим функцию, вычисляющую разницу между самой ранней и самой поздней датой:
CREATE OR REPLACE FUNCTION date_range(dates text[]) RETURNS integer AS $$ # Преобразуем строки в объекты Date date_vec <- as.Date(dates) if (length(date_vec) == 0) { plr.error("Массив дат пустой!") } min_date <- min(date_vec) max_date <- max(date_vec) day_diff <- as.integer(max_date - min_date) # Логируем промежуточные результаты plr.notice("Минимальная дата: " %+% min_date) plr.notice("Максимальная дата: " %+% max_date) plr.notice("Разница в днях: " %+% day_diff) return(day_diff) $$ LANGUAGE plr;
Вызов:
SELECT date_range(ARRAY['2023-01-01', '2023-02-15', '2023-03-10']) AS day_difference;
Совмещение массивов и возврата таблиц: нормализация данных
Иногда нужно одновременно принимать массивы, выполнять над ними вычисления и возвращать таблицу. Например, функция, которая принимает массивы имён и значений, нормализует значения и возвращает таблицу с исходными и нормализованными данными:
CREATE OR REPLACE FUNCTION normalize_values(names text[], values numeric[]) RETURNS TABLE(name text, original_value numeric, normalized_value numeric) AS $$ # Проверяем, что массивы совпадают по длине if (length(names) != length(values)) { plr.error("Массивы names и values должны иметь одинаковую длину") } name_vec <- as.character(names) value_vec <- as.numeric(values) # Нормализация: (x - min) / (max - min) min_val <- min(value_vec) max_val <- max(value_vec) normalized <- (value_vec - min_val) / (max_val - min_val) result <- data.frame( name = name_vec, original_value = value_vec, normalized_value = normalized ) return(result) $$ LANGUAGE plr;
Вызов:
SELECT * FROM normalize_values( ARRAY['Alpha', 'Beta', 'Gamma', 'Delta'], ARRAY[10, 20, 15, 25] );
Результатом станет таблица, готовая для дальнейшего анализа или визуализации.
Примеры применения PL/R
Линейная регрессия для прогнозирования продаж
Предположим, есть таблица с данными по продажам:
CREATE TABLE sales ( sale_date DATE NOT NULL, amount NUMERIC NOT NULL ); INSERT INTO sales (sale_date, amount) VALUES ('2023-01-01', 100), ('2023-01-02', 120), ('2023-01-03', 130), ('2023-01-04', 150), ('2023-01-05', 160), ('2023-01-06', 170), ('2023-01-07', 200);
Функция линейной регрессии принимает два массива — даты и суммы продаж, преобразует даты в числовой формат, строит модель и возвращает коэффициенты:
CREATE OR REPLACE FUNCTION linear_regression(dates date[], amounts numeric[]) RETURNS TABLE(intercept numeric, slope numeric) AS $$ numeric_dates <- as.numeric(as.Date(dates)) numeric_amounts <- as.numeric(amounts) model <- lm(numeric_amounts ~ numeric_dates) coefs <- coef(model) return(data.frame(intercept = coefs[1], slope = coefs[2])) $$ LANGUAGE plr;
Вызов:
WITH data AS ( SELECT array_agg(sale_date ORDER BY sale_date) AS dates, array_agg(amount ORDER BY sale_date) AS amounts FROM sales ) SELECT * FROM data, linear_regression(dates, amounts);
Получив коэффициенты, можно прогнозировать будущие продажи, просто подставив нужную дату в формулу.
Прогнозирование с ARIMA с использованием пакета forecast
Если нужно предсказать будущие значения временного ряда, можно использовать модель ARIMA. Пример функции, которая принимает массив сумм продаж и возвращает прогноз на заданное количество периодов:
CREATE OR REPLACE FUNCTION forecast_arima(amounts numeric[], forecast_period integer) RETURNS numeric[] AS $$ if (!require("forecast", quietly = TRUE)) { install.packages("forecast", repos="http://cran.r-project.org") library(forecast) } ts_data <- ts(as.numeric(amounts), frequency = 7) model <- auto.arima(ts_data) fc <- forecast(model, h = forecast_period) return(as.numeric(fc$mean)) $$ LANGUAGE plr;
Запуск:
WITH daily_sales AS ( SELECT array_agg(amount ORDER BY sale_date) AS amounts FROM sales WHERE sale_date BETWEEN '2023-01-01' AND '2023-01-07' ) SELECT forecast_arima(amounts, 7) AS predicted_sales FROM daily_sales;
Так можно оперативно получить прогноз продаж, не покидая базы данных.
Кластеризация геоданных: алгоритм k-means
Рассмотрим задачу кластеризации пользователей по координатам. Допустим, есть таблица с данными пользователей:
CREATE TABLE users ( id serial PRIMARY KEY, name text, longitude double precision, latitude double precision, active boolean DEFAULT true ); INSERT INTO users (name, longitude, latitude) VALUES ('User1', 37.62, 55.75), ('User2', 37.60, 55.76), ('User3', 37.64, 55.74), ('User4', 37.65, 55.73), ('User5', 37.70, 55.72);
Функция для кластеризации:
CREATE OR REPLACE FUNCTION kmeans_clustering(coords double precision[][], clusters integer) RETURNS TABLE(cluster_id integer, centroid_x double precision, centroid_y double precision) AS $$ if (length(coords) == 0) { plr.error("Пустой массив координат") } data_matrix <- do.call(rbind, coords) km_result <- kmeans(data_matrix, centers = clusters) centers <- km_result$centers result <- data.frame( cluster_id = 1:nrow(centers), centroid_x = centers[,1], centroid_y = centers[,2] ) return(result) $$ LANGUAGE plr;
Вызов:
WITH user_coords AS ( SELECT array_agg(ARRAY[longitude, latitude]) AS coords FROM users WHERE active = true ) SELECT * FROM user_coords, kmeans_clustering(coords, 2);
Результатом станут кластеры с координатами центроидов.
Классификация клиентов с randomForest
В качестве бонуса — пример машинного обучения для классификации клиентов. Допустим, есть таблица клиентов:
CREATE TABLE customers ( id serial PRIMARY KEY, age integer, income numeric, loyalty_score numeric, churn boolean ); INSERT INTO customers (age, income, loyalty_score, churn) VALUES (25, 30000, 0.7, false), (45, 70000, 0.9, false), (35, 50000, 0.5, true), (50, 90000, 0.8, false), (30, 40000, 0.6, true);
Функция для классификации на основе randomForest:
CREATE OR REPLACE FUNCTION classify_customers( ages integer[], incomes numeric[], scores numeric[], churns boolean[] ) RETURNS TABLE(id integer, predicted_churn integer) AS $$ if (!require("randomForest", quietly = TRUE)) { install.packages("randomForest", repos="http://cran.r-project.org") library(randomForest) } df <- data.frame( age = as.numeric(ages), income = as.numeric(incomes), loyalty = as.numeric(scores), churn = as.factor(churns) ) model <- randomForest(churn ~ age + income + loyalty, data = df, ntree = 100) predictions <- predict(model, df) result <- data.frame(id = 1:length(predictions), predicted_churn = as.integer(predictions) - 1) return(result) $$ LANGUAGE plr;
Вызов функции:
WITH customer_data AS ( SELECT array_agg(age ORDER BY id) AS ages, array_agg(income ORDER BY id) AS incomes, array_agg(loyalty_score ORDER BY id) AS scores, array_agg(churn ORDER BY id) AS churns FROM customers ) SELECT * FROM customer_data, classify_customers(ages, incomes, scores, churns);
В заключение напоминаю про открытые уроки по PostgreSQL, которые пройдут в марте в Otus:
-
4 марта. От PostgreSQL к Arenadata DB. Подробнее
-
24 марта. Безопасность в PostgreSQL: защита данных, управление доступом и аудит. Подробнее
Больше уроков по аналитике и не только смотрите в календаре мероприятий.
ссылка на оригинал статьи https://habr.com/ru/articles/885460/
Добавить комментарий