Отметьте несмежные диапазоны дат - PullRequest
8 голосов
/ 06 мая 2011

Фон (вход)

Глобальная историческая климатологическая сеть пометила недействительные или ошибочные данные при сборе измерений погоды. После удаления этих элементов есть ряд данных, которые больше не имеют последовательных дат. Данные напоминают:

"2007-12-01";14 -- Start of December
"2007-12-29";8
"2007-12-30";11
"2007-12-31";7
"2008-01-01";8 -- Start of January
"2008-01-02";12
"2008-01-29";0
"2008-01-31";7
"2008-02-01";4 -- Start of February
... entire month is complete ...
"2008-02-29";12
"2008-03-01";14  -- Start of March
"2008-03-02";17
"2008-03-05";17

Проблема (вывод)

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

D;"2007-12-01";14 -- Start of December
D;"2007-12-29";8
D;"2007-12-30";11
D;"2007-12-31";7
D;"2008-01-01";8 -- Start of January
D;"2008-01-02";12
D;"2008-01-29";0
D;"2008-01-31";7
"2008-02-01";4 -- Start of February
... entire month is complete ...
"2008-02-29";12
D;"2008-03-01";14  -- Start of March
D;"2008-03-02";17
D;"2008-03-05";17

Некоторые измерения были проведены в 1843 году.

Вопрос

Для всех метеостанций, как бы вы отметили все дни в месяцах, в которых пропущен один или несколько дней?

Исходный код

Код для выбора данных напоминает:

select
  m.id,
  m.taken,
  m.station_id,
  m.amount
from
  climate.measurement

Похожие идеи

Создайте таблицу, заполненную непрерывными датами, и сравните их с датами измеренных данных.

Обновление

Проблема может быть воссоздана с использованием SQL в этом разделе.

Таблица

Таблица создается следующим образом:

CREATE TABLE climate.calendar
(
  id serial NOT NULL,
  n character varying(2) NOT NULL,
  d date NOT NULL,
  "valid" boolean NOT NULL DEFAULT true,
  CONSTRAINT calendar_pk PRIMARY KEY (id)
)
WITH (
  OIDS=FALSE
);

Генерация данных

Следующий SQL вставляет данные в таблицу (id [int], n ame [varchar], d ate [date], valid [boolean]):

insert into climate.calendar (n, d) 
    select 'A', (date('1982-01-1') + (n || ' days')::interval)::date cal_date
    from generate_series(0, date('2011-04-9') - date('1982-01-1') ) n
insert into climate.calendar (n, d) 
    select 'B', (date('1982-01-1') + (n || ' days')::interval)::date cal_date
    from generate_series(0, date('2011-04-9') - date('1982-01-1') ) n
insert into climate.calendar (n, d) 
    select 'C', (date('1982-01-1') + (n || ' days')::interval)::date cal_date
    from generate_series(0, date('2011-04-9') - date('1982-01-1') ) n
insert into climate.calendar (n, d) 
    select 'D', (date('1982-01-1') + (n || ' days')::interval)::date cal_date
    from generate_series(0, date('2011-04-9') - date('1982-01-1') ) n
insert into climate.calendar (n, d) 
    select 'E', (date('1982-01-1') + (n || ' days')::interval)::date cal_date
    from generate_series(0, date('2011-04-9') - date('1982-01-1') ) n
insert into climate.calendar (n, d) 
    select 'F', (date('1982-01-1') + (n || ' days')::interval)::date cal_date
    from generate_series(0, date('2011-04-9') - date('1982-01-1') ) n

Значения от 'A' до 'F' представляют названия метеостанций, которые проводили измерения в определенный день.

Удалить случайные строки

Удалить некоторые строки следующим образом:

delete from climate.calendar where id in (select id from climate.calendar order by random() limit 5000);

Попытка # 1

Следующее не переключает флаг valid на false для всех дней месяца, когда месяц пропускает один или несколько дней:

UPDATE climate.calendar
SET valid = false
WHERE date_trunc('month', d) IN (
    SELECT DISTINCT date_trunc('month', d)
    FROM climate.calendar A
    WHERE NOT EXISTS (
        SELECT 1
        FROM climate.calendar B
        WHERE A.d - 1 = B.d
   )
);

Попытка # 2

Следующий SQL создает пустой набор результатов:

with gen_calendar as (
    select (date('1982-01-1') + (n || ' days')::interval)::date cal_date
    from generate_series(0, date('2011-04-9') - date('1982-01-1') ) n
)
select gc.cal_date
from gen_calendar gc
left join climate.calendar c on c.d = gc.cal_date
where c.d is null;

Попытка # 3

Следующий SQL генерирует все возможные комбинации имен станций и дат:

select
  distinct( cc.n ), t.d
from
  climate.calendar cc,
  (
    select (date('1982-01-1') + (n || ' days')::interval)::date d
    from generate_series(0, date('2011-04-9') - date('1982-01-1') ) n
  ) t
order by
  cc.n

Однако в реальных данных имеется несколько сотен станций, и даты восходят к середине 1800-х годов, поэтому декартово все даты для всех станций слишком велики. Такой подход может сработать, если у вас будет достаточно времени ... Должен быть более быстрый путь.

Попытка # 4

PostgreSQL имеет функции управления окнами.

Как выбрать конкретные изменения с помощью оконных функций в postgres

Спасибо!

Ответы [ 3 ]

3 голосов
/ 06 мая 2011

generate_series ()

Функция PostgreSQL generate_series() может создавать представление, содержащее последовательный список дат:

with calendar as (
    select ((select min(date) from test)::date + (n || ' days')::interval)::date cal_date
    from generate_series(0, (select max(date) - min(date) from test)) n
)
select cal_date
from calendar c
left join test t on t.date = c.cal_date
where t.date is null;

Выражение select max(date) - min(date) from test может быть отключено на единицу.

Количество дней в месяце

Одним из способов определения недействительных месяцев является создание двух представлений. Первый подсчитывает количество ежедневных показаний, которые каждая станция должна производить в каждом месяце. (Обратите внимание, что climate.calendar переводится в climate_calendar.) Секунда возвращает фактические ежедневные показания, производимые каждой станцией в месяц.

Максимальное количество дней в месяце на станцию ​​

В этом представлении будет отображаться фактическое количество дней в месяце для каждой станции. (Например, в феврале всегда будет 28 или 29 дней.)

create view count_max_station_calendar_days as 
with calendar as (
    select ((select min(d) from climate_calendar)::date + (n || ' days')::interval)::date cal_date
    from generate_series(0, (select max(d) - min(d) from climate_calendar)) n
)
select n, extract(year from cal_date) yr, extract(month from cal_date) mo, count(*) num_days
from stations cross join calendar
group by n, yr, mo
order by n, yr, mo

фактических дней в месяце на станцию ​​

Общее количество возвращенных дней будет меньше, чем подсчет. (Например, в январе всегда будет 31 день или меньше.)

create view count_actual_station_calendar_days as
select n, extract(year from d) yr, extract(month from d) mo, count(*) num_days
from climate_calendar
group by n, yr, mo
order by n, yr, mo;

Отбросьте пункты ORDER BY в производстве (они полезны при разработке).

Сравнение просмотров

Соедините два вида, чтобы определить станции и месяцы, которые необходимо пометить, в новом виде:

create view invalid_station_months as 
select m.n, m.yr, m.mo, m.num_days - a.num_days num_days_missing
from count_max_station_calendar_days m
inner join count_actual_station_calendar_days a
       on (m.n = a.n and m.yr = a.yr and m.mo = a.mo and m.num_days <> a.num_days)

n   yr    mo  num_days_missing
--
A   1982  1   1
E   2007  3   1

Столбец num_days_missing не обязателен, но полезен.

Это строки, которые необходимо обновить:

select cc.* 
from climate_calendar cc
inner join invalid_station_months im 
        on (cc.n = im.n and 
            extract(year from cc.d) = im.yr and
            extract(month from cc.d) = im.mo)
where valid = true

Обновление базы данных

Для их обновления удобна клавиша id.

update climate_calendar
set valid = false
where id in (
    select id
    from climate_calendar cc
    inner join invalid_station_months im 
        on (cc.n = im.n and 
            extract(year from cc.d) = im.yr and
            extract(month from cc.d) = im.mo)
    where valid = true
);
0 голосов
/ 08 мая 2011

Если предположить, что не может быть более одной строки в день, это должно вернуть все месяцы, для которых количество строк не равно количеству дней в месяце.

SELECT station_id, DATE_TRUNC('month', d)
FROM climate.calendar
GROUP BY station_id, DATE_TRUNC('month', d)
HAVING COUNT(*) <> 
  DATE_PART('month',
            DATE_TRUNC('month', d) + INTERVAL '1 month' - INTERVAL '1 day')
0 голосов
/ 06 мая 2011

Вот один из способов сделать это, предполагая, что у вас есть BOOLEAN-поле с именем is_contiguous. При необходимости измените:

UPDATE measurement
SET is_contiguous = FALSE
WHERE NOT EXISTS (
  SELECT 1
    FROM measurement B
   WHERE measurement.taken - 1 = B.taken
);

Edit:

Мне кажется, я неправильно понял ваше требование. Я думал, что вы хотели отметить отдельные даты, которые не были смежными. Но, очевидно, вы хотите пометить даты за целый месяц как непрерывные, если в них пропущено любое количество дней.

Редактировать 2:

Вот модифицированная версия моего исходного (неправильного) запроса, в котором выбираются отдельные месяцы, которые отсутствуют в любых днях:

UPDATE measurement
SET is_contiguous = FALSE
WHERE date_trunc('month', taken) IN (
    SELECT DISTINCT date_trunc('month', taken)
    FROM measurement A
    WHERE NOT EXISTS (
        SELECT 1
        FROM measurement B
        WHERE A.taken - 1 = B.taken
   )
);
...