Заполните пропущенные данные из Queryset Django - PullRequest
2 голосов
/ 26 января 2020

Я унаследовал приложение AngularJS / Django, используя DjangoRestFramework и базу данных Postgres, которая преобразовывается из AngularJS в React / Redux. Одна из вещей, которую мы пытаемся сделать, это представить данные временных рядов с помощью amCharts4. Проблема (среди многих других), с которой мы сталкиваемся, заключается в представлении данных за промежуток времени, для которого в БД может не быть записей. Например, у нас есть результаты, которые могут выглядеть примерно так:

[
    {
        "date": "2020-01-16T00:00:00.000Z",
        "result": 3
    },
    {
        "date": "2020-01-18T00:00:00.000Z",
        "result": 2
    }
]

И мы хотели бы, чтобы они выглядели примерно так:

[
    {
        "date": "2020-01-16T00:00:00.000Z",
        "result": 3
    },
    {
        "date": "2020-01-17T00:00:00.000Z",
        "result": 0
    },
    {
        "date": "2020-01-18T00:00:00.000Z",
        "result": 2
    }
]

Кроме того, у нас также есть данные с несколькими точками данных на Событие времени:

[
    {
        "date": "2020-01-13T00:00:00Z",
        "result": 1,
        "name": "Yes"
    },
    {
        "date": "2020-01-14T00:00:00Z",
        "result": 1,
        "name": "No"
    },
    {
        "date": "2020-01-16T00:00:00Z",
        "result": 1,
        "name": "No"
    }
]

И хотелось бы, чтобы данные заполнялись нулями для любого name в любую дату, когда нет результата:

[
    {
        "date": "2020-01-13T00:00:00Z",
        "result": 1,
        "name": "Yes"
    },
    {
        "date": "2020-01-13T00:00:00Z",
        "result": 0,
        "name": "No"
    },
    {
        "date": "2020-01-14T00:00:00Z",
        "result": 0,
        "name": "Yes"
    },
    {
        "date": "2020-01-14T00:00:00Z",
        "result": 1,
        "name": "No"
    },
    {
        "date": "2020-01-15T00:00:00Z",
        "result": 0,
        "name": "Yes"
    },
    {
        "date": "2020-01-15T00:00:00Z",
        "result": 0,
        "name": "No"
    },
    {
        "date": "2020-01-16T00:00:00Z",
        "result": 0,
        "name": "Yes"
    },
    {
        "date": "2020-01-16T00:00:00Z",
        "result": 1,
        "name": "No"
    }
]

Диапазон этих результаты также не обязательно зависят от даты начала и окончания даты, но могут быть указаны пользователем. В этом случае нам потребуется заполнить результаты с нулевым значением для всех параметров всех дат в этих диапазонах.

Мне известно о свойстве amCharts skipEmptyPeriods ( amCharts4 - skipEmptyPeriods ) , но мои инженеры по внешнему интерфейсу сказали мне, что это не сработает для случаев с несколькими линиями тренда (т. е. для второго случая, когда существует несколько опций на дату). Кроме того, на самом деле это не проблема внешнего интерфейса, а проблемы с производительностью.

Кроме того, я попытался использовать функцию Postgresql generate_series с coalesce Postgresql - generate_series , но не смог заставить это работать для второго случая.

В настоящее время я пытаюсь это сделать в Pandas (который я никогда не использовал) и у меня есть решение для Первая проблема с единичными записями на дату, но, опять же, и проблема со вторым случаем нескольких записей на дату:

    from_date = request.query_params.get("from_date")
    to_date = request.query_params.get("to_date")

    # let's do some zero plotting
    filtered_queryset = list(filtered_queryset)
    if from_date:
        from_date = datetime.strptime(from_date, "%Y-%m-%d").astimezone(pytz.UTC)
    else:
        from_date = filtered_queryset[0]["date"]
    if to_date:
        to_date = datetime.strptime(to_date, "%Y-%m-%d").astimezone(pytz.UTC)
        _now = localtime(now()).astimezone(pytz.UTC)
        to_date = min(to_date, _now)
    else:
        to_date = localtime(now()).astimezone(pytz.UTC)

    pandas_freq_map = {"day": "D", "week": "W-MON", "month": "MS"}
    freq = pandas_freq_map.get(request.query_params.get("frequency"))

    idx = pd.date_range(from_date.date(), to_date.date(), freq=freq)
    df = pd.DataFrame(list(filtered_queryset))
    datetime_series = pd.to_datetime(df["date"])
    datetime_index = pd.DatetimeIndex(datetime_series.values)

    df = df.set_index(datetime_index)
    df.drop("date", axis=1, inplace=True)
    df = df.asfreq(freq)
    df = df.reindex(idx, fill_value=0)
    df_json = json.JSONDecoder().decode(df.to_json(date_format="iso"))

    # this (result or 0) tomfoolery is bc I don't understand why pandas sometimes reindexes with null as the fill_value
    prepared_response = [{"date": date, "result": (result or 0)} for date, result in df_json["result"].items()]

Ответы [ 2 ]

1 голос
/ 26 января 2020

Ниже приведена попытка создать решение с помощью панды. По сути, вы можете выполнить повторную выборку и затем переиндексировать диапазон дат, но это немного затрудняет работу с составным индексом

Настройка данных

import pandas as pd
data = [    { "date": "2020-01-16T00:00:00.000Z", "result": 3 }, 
            { "date": "2020-01-18T00:00:00.000Z", "result": 2 }, 
            { "date": "2020-01-13T00:00:00Z", "result": 1, "name": "Yes" }, 
            { "date": "2020-01-14T00:00:00Z", "result": 1, "name": "No" }, 
            { "date": "2020-01-16T00:00:00Z", "result": 1, "name": "No" }]

# build dataframe
df = pd.DataFrame(data )
df.name = df.name.fillna("No")
df.date = pd.to_datetime( df.date)

Затем обработка данных

# set up date range
idx = pd.date_range( df.date.min() , df.date.max() , freq="H")

# resample yes/no for name separately
df = df.set_index(["name", "date"]).sort_index()

no = df.loc["No"].resample( rule="60min").sum().reset_index()
no["Name"] = ["No"] * len(no)
no.set_index( ["Name", "date"], inplace=True)

yes = df.loc["Yes"].resample( rule="60min").sum().reset_index()
yes["Name"] = ["Yes"] * len(yes)
yes.set_index( ["Name", "date"], inplace=True)

# reindex with the full date range
yes = yes.reindex(pd.MultiIndex.from_arrays([["Yes"]*len(idx), idx], names=('Name', 'date')), fill_value=0)
no = no.reindex(pd.MultiIndex.from_arrays([["No"]*len(idx), idx], names=('Name', 'date')), fill_value=0)

# merge and create output (dateformat has to be adjusted)
df = pd.concat( [yes, no], axis=0)
df.reset_index().to_dict('records')

Результат

[{'Name': 'Yes',
  'date': Timestamp('2020-01-13 00:00:00+0000', tz='UTC'),
  'result': 1},
 {'Name': 'Yes',
  'date': Timestamp('2020-01-13 01:00:00+0000', tz='UTC'),
  'result': 0}, ....
]
0 голосов
/ 26 января 2020

Продолжил решение Postgres и нашел рабочий запрос:

WITH
unnested_select AS (
    SELECT unnest(forms_completedformfield.value_text_array) as unnested_array,
           date_trunc('day', created) as created
    FROM forms_completedformfield
    WHERE forms_completedformfield.completed_survey_id =
        ANY(
            ARRAY['815251ac-3891-4206-b876-d17898b74e66'::uuid, '74aea6f5-9860-4fe5-8820-68a279726c83'::uuid, '173ea91f-0dc8-4a6c-b330-7c3cee13e1b4'::uuid]
        )
    GROUP BY unnested_array,
             created
),

range_counts AS (
    SELECT date_trunc('day', unnested_select.created) as date,
           count(unnested_select.unnested_array) as ct,
           unnested_select.unnested_array as ar
    FROM unnested_select
    WHERE unnested_select.unnested_array =
        ANY(
            ARRAY['2b0076f1-7be5-4e52-9879-47e4eeafe175']
        ) 
    GROUP BY unnested_select.unnested_array,
             unnested_select.created
),

range_sums AS (
    SELECT date_trunc('day', unnested_select.created) as date,
           count(unnested_select.unnested_array) as ct
    FROM unnested_select
    GROUP BY unnested_select.created
),

range_values AS ( 
    SELECT date_trunc('day', min(created)) as minval,
           date_trunc('day', max(created)) as maxval
    FROM unnested_select
),

frequency_range AS (
    SELECT generate_series(minval, maxval, '1 day'::interval) as date
    FROM range_values
),

field_options AS (
    SELECT
        DISTINCT unnested_select.unnested_array as ar,
        frequency_range.date
    FROM unnested_select
    CROSS JOIN frequency_range
)

SELECT  
        frequency_range.date as fd,
        field_options.ar as far,
        range_counts.ar as rar,
        range_counts.ct as ct
FROM frequency_range
LEFT OUTER JOIN field_options ON frequency_range.date = field_options.date
LEFT OUTER JOIN range_counts ON frequency_range.date = range_counts.date and field_options.ar = range_counts.ar
ORDER BY 
        frequency_range.date

Очевидно, что жестко закодированные значения в ARRAY s будут заменены.

...