PostgreSQL: interpolar o valor ausente

Aug 19 2020

Eu tenho uma tabela no PostgreSQL com um carimbo de data / hora e um valor.
Eu gostaria de interpolar os valores ausentes em "lat".

Os valores em "lat" são alturas das marés acima de um datum. Para o efeito, não há problema em interpolar o valor faltante linear entre os dois valores conhecidos.

Qual é o melhor método para fazer isso no PostgreSQL?

Editar 20200825

Eu resolvi esse problema de uma maneira diferente usando o calculador de campo QGIS. Problema com esse método: demora muito e o processo roda do lado do cliente e gostaria de rodar diretamente no servidor.

Em etapas, meu fluxo de trabalho foi:

  1. O intervalo entre os valores "lat" registrados é de 10 minutos. Calculei o incremento por minuto entre dois valores registrados e armazenei isso em uma coluna extra chamada "tidal_step" no valor "lat" registrado. (eu armazenei o carimbo de data / hora também como uma "época" em uma coluna)

No QGIS:

tidal_step =
-- the lat value @ the epoch, 10 minutes or 600000 miliseconds from the current epoch: 
(attribute(get_feature('werkset','epoch',("epoch"+'600000')),'lat') -
-- the lat value @ the current
attribute(get_feature('werkset','epoch',"epoch"),'lat'))
/10

para os dois primeiros valores da imagem de exemplo que resulta em: (4,95 - 5,07) / 10 = -0,012

  1. Eu determinei a quantidade de minutos do valor "lat" a ser interpolado, após a última instância registrada onde um valor "lat" foi registrado e armazenei isso em uma coluna: "min_past_rec"

No QGIS:

left(
right("timestamp",8) --this takes the timestamp and goes 8 charakters from the right
,1) -- this takes the string from the previous right( and goes 1 character left

para o primeiro valor no exemplo: 2019-01-01 00:15:15 retorna: '5' Isto é 5 minutos após o último valor registrado.

  1. Eu interpolei os valores ausentes adicionando ("min_past_rec" * "tidal_step") ao último valor "lat" registrado e armazenei isso na coluna chamada "lat_interpolated"

No QGIS

CASE
WHEN "lat" = NULL 
THEN
-- minutes pas the last recorded instance:
("min_past_rec" *
-- the "tidal_step" at the last recorded "lat"-value:
(attribute(get_feature('werkset','epoch',
("epoch" - --the epoch of the "lat" value to be interpolated minus:

left(right("timestamp",8),1) * 600000 -- = the amount of minutes after the last recorded instance.
+ left(right("timestamp",6),2) * 1000) --  and the amount of seconds after the last recorded instance.
),'tidal_step')) +

-- the last recorded "lat"-value

(attribute(get_feature('werkset','epoch',("epoch" - left(right("timestamp",8),1) * 600000 + left(right("timestamp",6),2) * 1000)),'lat'))

Com dados do exemplo:

01/01/2019 00:17:33:

"lat_interpolated" = "min_past_rec" * "tidal_step" + "lat" = 
7*-0.012 + 4.95 = 4.866
  1. deletar colunas obsoletas do banco de dados

Quais instruções / script devo usar no PostgreSQL para realizar a mesma tarefa?

Respostas

1 Vérace Aug 19 2020 at 09:58

Tenho uma solução (parcial) - o que fiz foi o seguinte (veja o violino disponível aqui ):

O algoritmo que usei para interpolação foi

  • se houver uma sequência de 1 NULL, tire a média do valor acima e o valor abaixo.

  • Uma sequência de 2 NULLs, o valor atribuído superior é a média dos dois registros acima dele e o valor atribuído inferior é a média dos dois registros abaixo.

Para fazer isso, fiz o seguinte:

Crie uma tabela:

CREATE TABLE data 
(
  s SERIAL PRIMARY KEY, 
  t TIMESTAMP, 
  lat NUMERIC
);

Preencha-o com alguns dados de amostra:

INSERT INTO data (t, lat)
VALUES
('2019-01-01 00:00:00', 5.07),
('2019-01-01 01:00:00', 4.60),
('2019-01-01 02:00:00', NULL),
('2019-01-01 03:00:00', NULL),
('2019-01-01 04:00:00', 4.7),
('2019-01-01 05:00:00', 4.20),
('2019-01-01 06:00:00', NULL),
('2019-01-01 07:00:00', 4.98),
('2019-01-01 08:00:00', 4.50);

Observe que os registros 3, 4 e 7 são NULL.

E então executei minha primeira consulta:

WITH cte1 AS
(
  SELECT d1.s,
    d1.t AS t1, d1.lat AS l1,
    LAG(d1.lat, 2)  OVER (ORDER BY t ASC) AS lag_t1_2,
    LAG(d1.lat, 1)  OVER (ORDER BY t ASC) AS lag_t1,
    LEAD(d1.lat, 1) OVER (ORDER BY t ASC) AS lead_t1,
    LEAD(d1.lat, 2) OVER (ORDER BY t ASC) AS lead_t1_2
  FROM data d1
),
cte2 AS
(
  SELECT 
    d2.t AS t2, d2.lat AS l2, 
    LAG(d2.lat, 1) OVER(ORDER BY t DESC) AS lag_t2,
    LEAD(d2.lat, 1) OVER(ORDER BY t DESC) AS lead_t2
  FROM data d2
),
cte3 AS
(
  SELECT t1.s,
    t1.t1,  t1.lag_t1_2, t1.lag_t1, t2.lag_t2, t1.l1, t2.l2, 
    t1.lead_t1, t2.lead_t2, t1.lead_t1_2
  FROM cte1 t1
  JOIN cte2 t2
  ON t1.t1 = t2.t2
)
SELECT * FROM cte3;

Resultado (espaços significam NULL- é muito mais claro no violino):

s   t1  lag_t1_2    lag_t1  lag_t2  l1  l2  lead_t1     lead_t2     lead_t1_2
1   2019-01-01 00:00:00             4.60    5.07    5.07    4.60        
2   2019-01-01 01:00:00         5.07        4.60    4.60        5.07    
3   2019-01-01 02:00:00     5.07    4.60                    4.60    4.7
4   2019-01-01 03:00:00     4.60        4.7             4.7         4.20
5   2019-01-01 04:00:00             4.20    4.7     4.7     4.20        
6   2019-01-01 05:00:00         4.7         4.20    4.20        4.7     4.98
7   2019-01-01 06:00:00     4.7     4.20    4.98            4.98    4.20    4.50
8   2019-01-01 07:00:00     4.20        4.50    4.98    4.98    4.50        
9   2019-01-01 08:00:00         4.98        4.50    4.50        4.98 

Observe o uso das funções LAG()e LEAD()Window ( documentation). Eu os usei na mesma mesa, mas classificados de forma diferente.

Isso e usar a OFFSETopção significa que da minha única latcoluna original , agora tenho 6 colunas extras de dados "gerados" que são muito úteis para atribuir valores aos NULLvalores ausentes . A última peça (parcial) do quebra-cabeça é mostrada abaixo (a consulta SQL completa está no final desta postagem e também no violino).

cte4 AS
(
  SELECT t1.s,
  t1.l1 AS lat,
    CASE 
      WHEN (t1.l1 IS NOT NULL) THEN t1.l1
      WHEN (t1.l1 IS NULL) AND (t1.l2) IS NULL AND (t1.lag_t1 IS NOT NULL)
        AND (t1.lag_t2 IS NOT NULL) THEN ROUND((t1.lag_t1 + t1.lag_t2)/2, 2) 
      WHEN (t1.lag_t2 IS NULL) AND (t1.l1 IS NULL) AND (t1.l2 IS NULL) 
        AND (t1.lead_t1 IS NULL) THEN ROUND((t1.lag_t1 + t1.lag_t1_2)/2, 2)
      WHEN (t1.l1 IS NULL) AND (t1.l2 IS NULL)  AND (t1.lag_t1 IS NULL)
        AND (t1.lead_t2 IS NULL) THEN ROUND((t1.lead_t1 + t1.lead_t1_2)/2, 2)
      ELSE 0
    END AS final_val
  FROM cte3 t1
)
SELECT s, lat, final_val FROM cte4;

Resultado final:

s    lat    final_val
1   5.07         5.07
2   4.60         4.60
3   NULL         4.84
4   NULL         4.45
5   4.7           4.7
6   4.20         4.20
7   NULL         4.59
8   4.98         4.98
9   4.50         4.50

Então, você pode ver que o valor calculado para o registro 7 é a média dos registros 6 e 8 e o registro 3 é a média dos registros 1 e 2 e o valor atribuído para o registro 4 é a média de 5 e 6. Isso foi habilitado por o uso da OFFSETopção para as funções LAG()e LEAD(). Se você obtiver sequências de 3 NULLs, terá que usar um OFFSETde 3 e assim por diante.

Não estou muito feliz com essa solução - ela envolve codificação permanente para o número de se NULLessas CASEinstruções se tornarão ainda mais complexas e horríveis. Idealmente, algum tipo de RECURSIVE CTEsolução é necessária, mas eu HTH!

=================================== Consulta Completa ================= =======

WITH cte1 AS
(
  SELECT d1.s,
    d1.t AS t1, d1.lat AS l1,
    LAG(d1.lat, 2)  OVER (ORDER BY t ASC) AS lag_t1_2,
    LAG(d1.lat, 1)  OVER (ORDER BY t ASC) AS lag_t1,
    LEAD(d1.lat, 1) OVER (ORDER BY t ASC) AS lead_t1,
    LEAD(d1.lat, 2) OVER (ORDER BY t ASC) AS lead_t1_2
  FROM data d1
),
cte2 AS
(
  SELECT 
    d2.t AS t2, d2.lat AS l2, 
    LAG(d2.lat, 1) OVER(ORDER BY t DESC) AS lag_t2,
    LEAD(d2.lat, 1) OVER(ORDER BY t DESC) AS lead_t2
  FROM data d2
),
cte3 AS
(
  SELECT t1.s,
    t1.t1,  t1.lag_t1_2, t1.lag_t1, t2.lag_t2, t1.l1, t2.l2, 
    t1.lead_t1, t2.lead_t2, t1.lead_t1_2
  FROM cte1 t1
  JOIN cte2 t2
  ON t1.t1 = t2.t2
),
cte4 AS
(
  SELECT t1.s,
  t1.l1 AS lat,
    CASE 
      WHEN (t1.l1 IS NOT NULL) THEN t1.l1
      WHEN (t1.l1 IS NULL) AND (t1.l2) IS NULL AND (t1.lag_t1 IS NOT NULL)
        AND (t1.lag_t2 IS NOT NULL) THEN ROUND((t1.lag_t1 + t1.lag_t2)/2, 2) 
      WHEN (t1.lag_t2 IS NULL) AND (t1.l1 IS NULL) AND (t1.l2 IS NULL) 
        AND (t1.lead_t1 IS NULL) THEN ROUND((t1.lag_t1 + t1.lag_t1_2)/2, 2)
      WHEN (t1.l1 IS NULL) AND (t1.l2 IS NULL)  AND (t1.lag_t1 IS NULL)
        AND (t1.lead_t2 IS NULL) THEN ROUND((t1.lead_t1 + t1.lead_t1_2)/2, 2)
      ELSE 0
    END AS final_val,
    t1.lead_t1_2
  FROM cte3 t1
)
SELECT s, lat, final_val, lead_t1_2 FROM cte4;