2017-09-28 4 views
0

jeux de données graphique de croissance CDC fournissent un bel exemple de ce que je suis en train d'accomplir: http://www.cdc.gov/growthcharts/html_charts/statage.htmComment interpoler linéairement une valeur dans une table en fonction d'une table de recherche différente dans PostgreSQL?

On suppose que leurs tables ont été transposées dans la forme suivante:

Table cdc avec des colonnes: chart_label, le sexe, l'âge, la protéine tau, la valeur

with tmp (chart_label, sex, age, tau, val) as (values 
('bmi for age','F',2,0.03,14.14735), 
('bmi for age','F',2,0.05,14.39787), 
('bmi for age','F',2,0.1,14.80134), 
('bmi for age','F',2,0.25,15.52808), 
('bmi for age','F',2,0.5,16.4234), 
('bmi for age','F',2,0.75,17.42746), 
('bmi for age','F',2,0.85,18.01821), 
('bmi for age','F',2,0.9,18.44139), 
('bmi for age','F',2,0.95,19.10624), 
('bmi for age','F',2,0.97,19.56411), 
('bmi for age','F',2.041667,0.03,14.13226), 
('bmi for age','F',2.041667,0.05,14.38019), 
('bmi for age','F',2.041667,0.1,14.77965), 
('bmi for age','F',2.041667,0.25,15.49976), 
('bmi for age','F',2.041667,0.5,16.38804), 
('bmi for age','F',2.041667,0.75,17.38582), 
('bmi for age','F',2.041667,0.85,17.97371), 
('bmi for age','F',2.041667,0.9,18.39526), 
('bmi for age','F',2.041667,0.95,19.05824), 
('bmi for age','F',2.041667,0.97,19.51534)) 
select * from tmp; 

Je veux écrire une fonction PostgreSQL pour renvoyer le tau estimé pour un tableau donné, le sexe, l'âge et la valeur par interpolation linéaire pour estimer le tau s'il n'y a pas de valeur exacte disponible pour les entrées.

Par exemple (code pseudo):

select interp('bmi for age', 'F', 2.02, 15); 

doit renvoyer une valeur tau entre 0,1 et 0,25 (à peu près 0,141) puisqu'il sera l'interpolation entre ces deux lignes:

('bmi for age','F',2,0.1,14.80134), 
('bmi for age','F',2,0.25,15.52808), 

Je réalise que l'interpolation linéaire peut ne pas être la solution idéale pour trouver le percentile approprié, mais comme je l'ai dit, les diagrammes de croissance des CDC sont une approximation appropriée de mon cas d'utilisation réel.

La seule chose que je devais aller était this post, et ces autres questions similaires sur SO link 1 et link 2

Répondre

0

je suis venu avec quelques solutions basées sur diverses recherches sur le SO, des liens dans la question, et la documentation . La chose malheureuse à propos de chacune de ces solutions est qu'elles sont relativement lentes parce que la recherche est appelée une fois par valeur.

En outre, chacun pourrait probablement être amélioré avec la gestion des erreurs, la validation des entrées, et une meilleure logique dans le traitement des conditions aux limites. Pour l'instant, je retourne juste les valeurs extrêmes basses/hautes si la valeur demandée est au-delà de l'échelle de la table.

Solution SQL:

create or replace function cdcInterp(_valtype text, 
            _insex character(1), 
            _inage numeric, 
            _inval numeric) 
-- _valtype should be one of either 'bmi for age', 'wt for age', or 'ht for age' 
-- _insex should be one of either 'M' or 'F' 
returns numeric as 
    $$ 
-- make a lookup table 
with lkup as (
    select * 
    from cdc_chart_value 
    where chart_label = _valtype 
    and sex = _insex 
    order by abs(age - _inage) asc, age, tau 
    -- order by ensures that I am using the closest age, 
    -- with ties defaulting to the younger age 
    -- 10 is a magic number: it is the number of taus for each age 
    -- (0.03, 0.05, 0.10, 0.25, 0.50, 0.75, 0.85, 0.90, 0.95, 0.97) 
    limit 10 
), 
-- find high and low values needed to do interpolation 
    vals as (select 
      -- x1 is the lower value 
      (SELECT lkup.val FROM lkup WHERE lkup.val <= _inval ORDER BY lkup.val DESC LIMIT 1) as x1, 
      -- x2 is the upper value 
      (SELECT lkup.val FROM lkup WHERE lkup.val >= _inval ORDER BY lkup.val ASC LIMIT 1) as x2, 
      -- y1 is the lower tau 
      (SELECT lkup.tau FROM lkup WHERE lkup.val <= _inval ORDER BY lkup.val DESC LIMIT 1) as y1, 
      -- y2 is the upper tau 
      (SELECT lkup.tau FROM lkup WHERE lkup.val >= _inval ORDER BY lkup.val ASC LIMIT 1) as y2 
      from lkup) 

-- interpolate, or not, as needed 
SELECT 
    CASE 
    WHEN vals.x1 = vals.x2 THEN vals.y1 -- if equal, then return the exact tau 
    when vals.x1 is null then vals.y2 -- if the lower value is null, then return the lowest tau (.03) 
    when vals.x2 is null then vals.y1 -- if the upper value is null, then returr the highest tau (.97) 
    ELSE    (vals.y1 + (_inval-vals.x1)/(vals.x2-vals.x1)*(vals.y2-vals.y1)) -- otherwise interpolate linearly 
    END AS y 
FROM vals 
$$ 
language sql stable; 

Ceci est un peu plus lent que ce que j'espérais (33 msec par requête). Vous vous demandez s'il y a un moyen de le faire plus vite?

solution plpgsql: (prend environ 50% plus longtemps que la solution SQL)

create or replace function interp2(_valtype text, 
            _insex character(1), 
            _inage numeric, 
            _inval numeric) 
returns numeric as 
$$ 
DECLARE 
    x1 numeric; 
    x2 numeric; 
    y1 numeric; 
    y2 numeric; 
    y numeric; 
begin 
    -- the overhead of creating/dropping a temporary table is bad 
    drop table if exists _tmp_lkup; 
    create temp table _tmp_lkup as 
    (select * 
     from cdc_chart_value 
     where chart_label = _valtype 
     and sex = _insex 
     order by abs(age - _inage) asc, age, tau 
     -- order by ensures that I am using the closest age, 
     -- with ties defaulting to the younger age 
     -- 10 is a magic number: it is the number of taus for each age 
     -- (0.03, 0.05, 0.10, 0.25, 0.50, 0.75, 0.85, 0.90, 0.95, 0.97) 
     limit 10 
    ); 
    x1 := (SELECT _tmp_lkup.val FROM _tmp_lkup WHERE _tmp_lkup.val <= _inval ORDER BY _tmp_lkup.val DESC LIMIT 1); 
    x2 := (SELECT _tmp_lkup.val FROM _tmp_lkup WHERE _tmp_lkup.val >= _inval ORDER BY _tmp_lkup.val ASC LIMIT 1); 
    y1 := (SELECT _tmp_lkup.tau FROM _tmp_lkup WHERE _tmp_lkup.val <= _inval ORDER BY _tmp_lkup.val DESC LIMIT 1); 
    y2 := (SELECT _tmp_lkup.tau FROM _tmp_lkup WHERE _tmp_lkup.val >= _inval ORDER BY _tmp_lkup.val ASC LIMIT 1); 

    -- interpolate, or not, as needed 
    y := (select CASE 
     WHEN x1 = x2 THEN y1 -- if equal, then return the exact tau 
     when x1 is null then y2 -- if the lower value is null, then return the lowest tau (.05) 
     when x2 is null then y1 -- if the upper value is null, then retunr the highest tau (.95) 
     ELSE    (y1 + (_inval-x1)/(x2-x1)*(y2-y1)) -- otherwise interpolate linearly 
     END); 
    return y; 
end; 
$$ language plpgsql volatile; 

Je crois qu'une solution plus rapide serait de réduire le nombre de fois que la recherche est créée. par exemple, en utilisant une boucle plpgsql sur le sexe et en interpolant tous les points mâles, puis tous les points femelles, et en retournant l'union des deux ensembles de résultats?

Une autre solution possible pourrait être d'utiliser l'interpolation griddata trouvée dans l'extension python/scipy.