PL/R в PostgreSQL

от автора

Привет, Хабр!

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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *