Рассчитать средний обменный курс за период времени
В Django у меня есть модель, похожая на этот пример:
class Currency(models.Model):
name = models.CharField(max_length=3, unique=True)
full_name = models.CharField(max_length=20)
class ExchangeRate(models.Model):
currency = models.ForeignKey('Currency')
start_date = models.DateFiled()
end_date = models.DateField()
exchange_rate = models.DecimalField(max_digits=12, decimal_places=4)
Давайте упростим это и предположим, что у нас есть только одна валюта и ExchangeRate
Стол выглядит так:
+---------------------+-------------------+------------+------------+---------------+
| currency_from__name | currency_to__name | start_date | end_date | exchange_rate |
+---------------------+-------------------+------------+------------+---------------+
| PLN | USD | 2014-03-01 | 2014-08-01 | 3.00000 |
| PLN | USD | 2014-08-01 | 2014-12-01 | 6.00000 |
+---------------------+-------------------+------------+------------+---------------+
Обратите внимание, что это пример для упрощения математических операций!
В этой таблице плотность данных составляет один раз в месяц, а действительная запись за один месяц, например, когда start_date = 2014.03.01
а также end_date = 2014.04.01
, так start_date
включительно и end_date
является эксклюзивным.
Я хочу рассчитать средний курс обмена за период времени:
Когда в Джанго я пишу:
start_date = date(2014, 6, 1)
end_date = date(2014, 9, 1)
ExchangeRate.objects.all().filter(
(
Q(start_date__lt=start_date) &
Q(end_date__gt=start_date)
) | (
Q(start_date__gte=start_date) &
Q(start_date__lt=end_date) &
Q(end_date__gt=start_date)
)
).annotate(
currency_from_name = 'currency_from__name',
currency_to_name = 'currency_to__name'
).values( # GROUP BY
'currency_from_name',
'currency_to_name'
).aggregate(
F('currency_from_name'),
F('currency_to_name'),
Avg('exchange_rate')
)
После этого запроса я получаю значение 4.5000
что с математической точки зрения правильно, но неправильно, когда вам нужно позаботиться о временном диапазоне.
Правильный ответ 4.000
,
Я только предложил это решение, чтобы аннотировать дополнительный столбец с помощью этой формулы, а затем вычислять среднее значение из этого столбца:
Куда:
Abs
функция для абсолютного значенияabs()
months
это функция для расчета месяцев между двумя датамиmonths_between()
greater
,smaller
являются функциями для выбора соответственно большего и меньшего значения из аргументов -greatest()
,least()
ER
означает столбец изExchangeRate
- напримерF('exchange_rate')
Я использую 9.3 PostgreSQL DB и Django 1.8.4.
Может быть, есть простая функция для этого?
Может я это слишком усложняю?
3 ответа
1. months_between()
:
create function months_of(interval)
returns int strict immutable language sql as $$
select extract(years from $1)::int * 12 + extract(month from $1)::int
$$;
create function months_between(date, date)
returns int strict immutable language sql as $$
select months_of(age($1, $2))
$$;
2. average_weight():
create function average_weight(numeric, date, date, date, date)
returns numeric(9,2) strict immutable language sql as $$
select abs(months_between(GREATEST($2, $4), LEAST($3, $5))/months_between($4, $5))*$1
$$;
3. AverageWeight:
from django.db.models.aggregates import Func
from django.db.models.fields import FloatField
class AverageWeight(Func):
function = 'average_weight'
def __init__(self, *expressions):
super(AverageWeight, self).__init__(*expressions, output_field=FloatField())
По вашему мнению:
ExchangeRate.objects.all().filter(
(
Q(start_date__lt=start_date) &
Q(end_date__gt=start_date)
) | (
Q(start_date__gte=start_date) &
Q(start_date__lt=end_date) &
Q(end_date__gt=start_date)
)
).annotate(
currency_from_name = 'currency_from__name',
currency_to_name = 'currency_to__name',
weight_exchange = AverageWeight(
F('exchange_rate'),
start_date,
end_date,
F('start_date'),
F('end_date'),
)
).values( # GROUP BY
'currency_from_name',
'currency_to_name'
).aggregate(
F('currency_from_name'),
F('currency_to_name'),
Avg('weight_exchange')
)
Проблема с вашим приложением заключается в том, как вы решаете хранить курсы валют. Итак, чтобы ответить на ваш вопрос: да, вы слишком усложняете это.
"Математика" говорит вам, что средний обменный курс составляет 4,5, потому что
(3 + 6) /2 == 4.5
Независимо от того, какую дату начала или окончания вы выберете, система получит одно и то же значение.
Чтобы устранить первопричину, давайте попробуем другой подход. (для простоты я оставлю внешние ключи и другие детали, не относящиеся к получению среднего значения в пределах определенного диапазона дат, вы можете добавить их позже)
с этой моделью:
class ExchangeRate(models.Model):
currency1 = models.CharField(max_length=3)
currency2 = models.CharField(max_length=3)
start_date = models.DateField()
exchange_rate = models.DecimalField(max_digits=12, decimal_places=4)
и эти данные:
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-03-01', 3);
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-04-01', 3);
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-05-01', 3);
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-06-01', 3);
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-07-01', 3);
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-08-01', 6);
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-09-01', 6);
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-10-01', 6);
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-11-01', 6);
мы можем выполнить этот запрос:
from django.db.models import Avg
from datetime import date
first_date = date(2014, 6, 1)
last_date = date(2014, 9, 1)
er.models.ExchangeRate.objects.filter(
start_date__gte = first_date,
start_date__lt = last_date
).aggregate(Avg('exchange_rate'))
Чтобы получить этот вывод:
{'exchange_rate__avg': 4.0}
Вы должны думать об этом как о средневзвешенном значении, поэтому вам нужно рассчитать вес каждой строки и затем сложить все вместе.
Я не знаю достаточно Django, чтобы помочь вам там, но в SQL это было бы (я не могу проверить это сейчас, но я думаю, что это дает правильную идею):
SELECT SUM((LEAST(end_date, @end_date) - GREATEST(start_date, @start_date)) * exchange_rate) / (@end_date - @start_date) AS weighted_avg
FROM
ExchangeRate
WHERE
(start_date, end_date) OVERLAPS (@start_date, @end_date)
При этом используется оператор OVERLAPS, чтобы увидеть, перекрываются ли периоды. Я не уверен, есть ли ошибка в расчете веса на 1, но думаю, что это следует учитывать в определении входной переменной (@end_date = @end_date - 1)