Interpolation between two values in a single query

吃可爱长大的小学妹 提交于 2020-01-11 13:06:21

问题


I want to calculate a value by interpolating the value between two nearest neighbours. I have a subquery that returns the values of the neighbours and their relative distance, in the form of two columns with two elements.

Let's say:

(select ... as value, ... as distance 
 from [get some neighbours by distance] limit 2) as sub

How can I calculate the value of the point by linear interpolation? Is it possible to do that in a single query?

Example: My point has the neighbour A with value 10 at distance 1, and the neighbour B with value 20 at distance 4. The function should return a value 10 * 4 + 20 * 1 / 5 = 12 for my point.

I tried the obvious approach

select sum(value * (sum(distance)-distance)) / sum(distance)

which will fail because you cannot work with group clauses inside group clauses. Using another subquery returning the sum is not possible either, because then I cannot forward the individual values at the same time.


回答1:


This is an ugly hack (based on a abused CTE ;). The crux of it is that

value1 * distance2 + value2 * distance1

Can, by dividing by distance1*distance2, be rewritten to

value1/distance1 + value2/distance2

So, the products (or divisions) can stay inside their rows. After the summation, multiplying by (distance1*distance2) rescales the result to the desired output. Generalisation to more than two neighbors is left as an exercise to the reader.YMMV

DROP TABLE tmp.points;
CREATE TABLE tmp.points
    ( pname VARCHAR NOT NULL PRIMARY KEY
    , distance INTEGER NOT NULL
    , value INTEGER
    );

INSERT INTO tmp.points(pname, distance, value) VALUES
  ( 'A' , 1, 10 )
,  ( 'B' , 4, 20 )
,  ( 'C' , 10 , 1)
,  ( 'D' , 11 , 2)
  ;
WITH RECURSIVE twin AS (
    select 1::INTEGER AS zrank
    , p0.pname AS zname
    , p0.distance AS dist
    , p0.value AS val
    , p0.distance* p0.value AS prod
    , p0.value::float / p0.distance AS frac
    FROM tmp.points p0
    WHERE NOT EXISTS ( SELECT * FROM tmp.points px
        WHERE px.distance < p0.distance)
    UNION
    select 1+twin.zrank AS zrank
    , p1.pname AS zname
    , p1.distance AS dist
    , p1.value AS val
    , p1.distance* p1.value AS prod
    , p1.value::float / p1.distance AS frac
    FROM tmp.points p1, twin
    WHERE p1.distance > twin.dist
    AND NOT EXISTS ( SELECT * FROM tmp.points px
        WHERE px.distance > twin.dist
        AND px.distance < p1.distance
        )   
    )
-- SELECT * from twin ;
SELECT min(zname) AS name1, max(zname) AS name2
    , MIN(dist) * max(dist) *SUM(frac) / SUM(dist) AS score
    FROM twin
    WHERE zrank <=2
    ;

The result:

CREATE TABLE
INSERT 0 4
 name1 | name2 | score 
-------+-------+-------
 A     | B     |    12

Update: this one is a bit cleaner ... ties are still not handled (need a window function or a LIMIT 1 clause in the outer query for that)

WITH RECURSIVE twin AS (
    select 1::INTEGER AS zrank
    , p0.pname AS name1
    , p0.pname AS name2
    , p0.distance AS dist
    FROM tmp.points p0
    WHERE NOT EXISTS ( SELECT * FROM tmp.points px
        WHERE px.distance < p0.distance)
    UNION
    select 1+twin.zrank AS zrank
    , twin.name1 AS name1
    , p1.pname AS name2
    , p1.distance AS dist
    FROM tmp.points p1, twin
    WHERE p1.distance > twin.dist
    AND NOT EXISTS ( SELECT * FROM tmp.points px
        WHERE px.distance > twin.dist
        AND px.distance < p1.distance
        )
    )
SELECT twin.name1, twin.name2
    , (p1.distance * p2.value + p2.distance * p1.value) / (p1.distance+p2.distance)  AS score
    FROM twin
    JOIN tmp.points p1 ON (p1.pname = twin.name1)
    JOIN tmp.points p2 ON (p2.pname = twin.name2)
    WHERE twin.zrank =2
    ;



回答2:


If you actually want the point in between, there is a built-in way of doing that (but not an aggregate function):

SELECT center(box(x.mypoint,y.mypoint))
FROM   ([get some neighbours by distance] order by value limit 1) x
      ,([get some neighbours by distance] order by value offset 1 limit 1) y;

If you want the mean distance:

SELECT avg(x.distance)
FROM   ([get some neighbours by distance] order by value limit 2) as x

See geometrical function and aggregate functions in the manual.

Edit:

For the added example, the query could look like this:

SELECT (x.value * 4 + y.value) / 5 AS result
FROM   ([get some neighbours by distance] order by value limit 1) x
      ,([get some neighbours by distance] order by value offset 1 limit 1) y;

I added missing () to get the result you expect!

Or, my last stab at it:

SELECT y.x, y.x[1], (y.x[1] * 4 + y.x[2]) / 5 AS result
FROM  (
    SELECT ARRAY(
        SELECT value FROM tbl WHERE [some condition] ORDER BY value LIMIT 2
        ) x
    ) y

It would be so much easier, if you provided the full query and the table definitions.



来源:https://stackoverflow.com/questions/7885526/interpolation-between-two-values-in-a-single-query

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!