Saltar al contenido

Calcule el promedio móvil de 40 días wrt en un campo

Te damos la bienvenida a nuestro espacio, ahora hallarás la respuesta de lo que estabas buscando.

Solución:

No queda realmente claro a partir de la pregunta cuál es el papel de la call_type_id columna. Lo ignoraré hasta que lo aclares.

Sin funciones de ventana

Aquí hay una variante simple que no usa funciones de ventana en absoluto.

Asegúrese de que haya un índice en (call_dt_key, aes_raw).

CTE_Dates devuelve una lista de todas las fechas de la tabla y calcula el promedio de cada día. Esta average_current_day será necesario para el primer día. El servidor escaneará todo el índice de cualquier forma, por lo que calcular dicho promedio es barato.

Luego, para cada día distinto, utilizo una autounión para calcular el promedio de los 40 días anteriores. Esto volverá NULL para el primer día, que se reemplaza con average_current_day en la consulta principal.

No tiene que usar CTE aquí, solo hace que la consulta sea más fácil de leer.

WITH
CTE_Dates
AS
(
    SELECT
        call_dt_key
        ,call_dt_key - INTERVAL '41 day' AS dt_from
        ,call_dt_key - INTERVAL '1 day' AS dt_to
        ,AVG(test_aes.aes_raw) AS average_current_day
    FROM test_aes
    GROUP BY call_dt_key
)
SELECT
    CTE_Dates.call_dt_key
    ,COALESCE(prev40.average_40, CTE_Dates.average_current_day) AS average_40
FROM
    CTE_Dates
    LEFT JOIN LATERAL
    (
        SELECT AVG(test_aes.aes_raw) AS average_40
        FROM test_aes
        WHERE
                test_aes.call_dt_key >= CTE_Dates.dt_from
            AND test_aes.call_dt_key <= CTE_Dates.dt_to
    ) AS prev40 ON true
ORDER BY call_dt_key;

Resultado

|                call_dt_key |         average_40 |
|----------------------------|--------------------|
|  January, 01 2016 00:00:00 |                 15 |
|  January, 05 2016 00:00:00 |                 15 |
|  January, 10 2016 00:00:00 | 15.333333333333334 |
|  January, 15 2016 00:00:00 | 15.294117647058824 |
|  January, 16 2016 00:00:00 |               15.5 |
|  January, 20 2016 00:00:00 | 15.652173913043478 |
|  January, 21 2016 00:00:00 |               15.6 |
|  January, 31 2016 00:00:00 | 15.555555555555555 |
| February, 01 2016 00:00:00 | 15.517241379310345 |
| February, 10 2016 00:00:00 | 15.483870967741936 |
| February, 15 2016 00:00:00 | 15.652173913043478 |
| February, 26 2016 00:00:00 | 15.333333333333334 |
|    March, 04 2016 00:00:00 |                 15 |
|    March, 18 2016 00:00:00 |                 15 |

Aquí está SQL Fiddle.

Con el índice recomendado, esta solución no debería ser tan mala.


Hay una pregunta similar, pero para SQL Server (Suma móvil de rango de fechas usando funciones de ventana). Postgres parece apoyar RANGE con una ventana de tamaño especificado, mientras que SQL Server no lo hace en este momento. Por lo tanto, es probable que la solución para Postgres sea un poco más simple.

los key parte sería:

AVG(...) OVER (ORDER BY call_dt_key RANGE BETWEEN 41 PRECEDING AND 1 PRECEDING)

Para calcular el promedio móvil usando estas funciones de ventana, probablemente primero tendrá que llenar los espacios vacíos en las fechas, de modo que la tabla tenga al menos una fila para cada día (con NULL valores para aes_raw en estas filas ficticias).

...

Como Erwin Brandstetter señaló correctamente en su respuesta, en este momento (a partir de Postgres 9.5) el RANGE La cláusula en Postgres todavía tiene limitaciones similares a SQL Server. Los doctores dicen:

los valor PRECEDENTES y valor Actualmente, los casos SIGUIENTES solo están permitidos en el modo FILAS.

Entonces, este método con el RANGE anterior no funcionaría para usted incluso si usara Postgres 9.5.


Usar funciones de ventana

Puede utilizar los enfoques descritos en la pregunta para SQL Server anterior. Por ejemplo, agrupe sus datos en sumas diarias, agregue filas para los días que faltan, calcule el movimiento SUM y COUNT utilizando OVER con ROWS y luego calcular la media móvil.

Algo en estas líneas:

WITH
CTE_Dates
AS
(
    SELECT
        call_dt_key
        ,SUM(test_aes.aes_raw) AS sum_daily
        ,COUNT(*) AS cnt_daily
        ,AVG(test_aes.aes_raw) AS avg_daily
        ,LEAD(call_dt_key) OVER(ORDER BY call_dt_key) - INTERVAL '1 day' AS next_date
    FROM test_aes
    GROUP BY call_dt_key
)
,CTE_AllDates
AS
(
    SELECT
        CASE WHEN call_dt_key = dt THEN call_dt_key ELSE NULL END AS final_dt
        ,avg_daily
        ,SUM(CASE WHEN call_dt_key = dt THEN sum_daily ELSE NULL END) 
            OVER (ORDER BY dt ROWS BETWEEN 41 PRECEDING AND 1 PRECEDING)
        /SUM(CASE WHEN call_dt_key = dt THEN cnt_daily ELSE NULL END) 
            OVER (ORDER BY dt ROWS BETWEEN 41 PRECEDING AND 1 PRECEDING) AS avg_40
    FROM
        CTE_Dates
        INNER JOIN LATERAL
            generate_series(call_dt_key, COALESCE(next_date, call_dt_key), '1 day') 
            AS all_dates(dt) ON true
)
SELECT
    final_dt
    ,COALESCE(avg_40, avg_daily) AS final_avg
FROM CTE_AllDates
WHERE final_dt IS NOT NULL
ORDER BY final_dt;

El resultado es el mismo que en la primera variante. Consulte SQL Fiddle.

Nuevamente, esto podría escribirse con subconsultas en línea sin CTE.

Vale la pena verificar en datos reales el rendimiento de diferentes variantes.

La gran recompensa hace que la respuesta aceptada actualmente parezca ejemplar, pero no estoy del todo satisfecho con varios detalles. Por lo tanto, agregué esta respuesta.

Definición de tabla

Debería haber proporcionado una definición de tabla real para facilitar esto.

A juzgar por los datos de la muestra, call_dt_tm es tipo timestamp with time zone (timestamptz). La columna call_dt_key no es completamente funcionalmente dependiente, ya que la fecha coincidente depende de la zona horaria. Pero si define eso (no solo un desplazamiento, ¡cuidado con el horario de verano!), La fecha se puede derivar fácil y confiablemente de un timestamptz y debería no almacenarse de forma redundante. Para hacerlo bien, use una expresión como:

(call_dt_tm AT TIME ZONE 'Asia/Hong_Kong')::date  -- use your time zone name

Detalles:

  • Ignorar las zonas horarias por completo en Rails y PostgreSQL

Podrías agregar un MATERIALIZED VIEW con la columna de fecha derivada para facilitar su uso ...

Para el propósito de esta pregunta, me ceñiré a la tabla dada.

40 días

La pregunta y la respuesta cuentan 41 días en lugar de 40 según el requisito. Se incluyen los límites inferior y superior, lo que resulta en un error de uno por uno (bastante común).

En consecuencia, obtengo resultados diferentes en dos filas a continuación.

date, interval, timestamp

Restando un interval a partir de una date produce un timestamp (como en call_dt_key - INTERVAL '41 day'). Para el propósito de esta consulta, es más eficiente restar un integer, produciendo otro date (igual que call_dt_key - 41).

No posible con un RANGE cláusula

@Vladimir sugirió (ahora arreglado) una solución con el RANGE cláusula en la definición de marco de funciones de ventana en Postgres 9.5.

De hecho, nada ha cambiado entre Postgres 9.4 y 9.5 a este respecto, ni siquiera el texto del manual. La definición de marco de las funciones de ventana solo permite RANGE UNBOUNDED PRECEDING y RANGE UNBOUNDED FOLLOWING - no con valores.

Respuesta

Por supuesto, puede utilizar un CTE para calcular la suma / recuento / promedio diarios sobre la marcha. Pero tu mesa ...

almacena la información sobre las llamadas de los usuarios en un centro de llamadas

Este tipo de información no cambia más tarde. Así que calcule los agregados diarios una vez en una vista materializada y construir sobre eso.

CREATE MATERIALIZED VIEW mv_test_aes AS
SELECT call_dt_key       AS day
     , sum(aes_raw)::int AS day_sum
     , count(*)::int     AS day_ct
FROM   test_aes
WHERE  call_dt_key < (now() AT TIME ZONE 'Asia/Hong_Kong')::date  -- see above
GROUP  BY call_dt_key
ORDER  BY call_dt_key;

El día actual siempre falta, pero eso es un característica. Los resultados serían incorrectos antes de que termine el día.

El MV necesita ser actualizado una vez por día, antes de ejecutar la consulta o faltan los últimos días.

Un índice en la tabla subyacente es no es necesario para esto, ya que toda la tabla se lee de todos modos.

CREATE INDEX test_aes_day_val ON test_aes (call_dt_key, aes_raw);

Puede crear una vista materializada más inteligente a mano y solo agregue nuevos días de forma incremental en lugar de recrear todo con MV estándar. Pero eso está más allá del alcance de la pregunta ...

Sin embargo, sugiero encarecidamente un índice en el MV:

CREATE INDEX foo ON mv_test_aes (day, day_sum, day_ct);

Yo solo agregué day_sum y day_ct esperando escaneos de solo índice. Si no los ve en sus consultas, no necesita las columnas en el índice.

SELECT t.day
     , round(COALESCE(sum(t1.day_sum) * 1.0 / sum(t1.day_ct)  -- * 1.0 to avoid int division
                         , t.day_sum  * 1.0 /      t.day_ct), 4) AS avg_40days
FROM   mv_test_aes t
LEFT   JOIN mv_test_aes t1 ON t1.day <  t.day
                          AND t1.day >= t.day - 40  -- not 41
GROUP  BY t.day, t.day_sum, t.day_ct
ORDER  BY t.day;

Resultado:

day        | avg_40days
-----------+------------
2016-01-01 | 15.0000
2016-01-05 | 15.0000
2016-01-10 | 15.3333
2016-01-15 | 15.2941
2016-01-16 | 15.5000
2016-01-20 | 15.6522
2016-01-21 | 15.6000
2016-01-31 | 15.5556
2016-02-01 | 15.5172
2016-02-10 | 15.4839
2016-02-15 | 15.5556  -- correct results
2016-02-26 | 15.0000
2016-03-04 | 15.0000
2016-03-18 | 15.0000

Violín SQL.

Si ejecuta esto a menudo, envolvería todo el asunto en un MV para evitar cálculos repetidos.

Una solución con funciones de ventana y una cláusula de marco. ROWS BETWEEN .... también sería posible. Pero los datos de su ejemplo sugieren que no tiene valores para la mayoría de los días en el rango (muchos más espacios que islas), por lo que no espero que sea más rápido. Relacionado:

  • Suma móvil / recuento / promedio durante el intervalo de fechas

Más adelante puedes encontrar las críticas de otros usuarios, tú aún puedes dejar el tuyo si te apetece.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *