2016-09-21 18 views
7

Mam datatable z wieloma wierszami i chciałbym warunkowo grupować dwie kolumny, a mianowicie Begin i End. Te kolumny oznaczają pewien miesiąc, w którym osoba powiązana coś robiła. Oto przykładowe dane (można użyć R, aby czytać, albo znaleźć czystych tabele poniżej, jeśli nie używać R):Grupa kolejnych zakresów

# base: 
test <- read.table(
text = " 
1 A mnb USA prim 4 12 
2 A mnb USA x 13 15 
3 A mnb USA un 16 25 
4 A mnb USA fdfds 1 2 
5 B ghf CAN sdg 3 27 
6 B ghf CAN hgh 28 29 
7 B ghf CAN y 24 31 
8 B ghf CAN ghf 38 42 
",header=F) 
library(data.table) 
setDT(test) 
names(test) <- c("row","Person","Name","Country","add info","Begin","End") 
out <- read.table(
text = " 
1 A mnb USA fdfds 1 2 
2 A mnb USA - 4 25 
3 B ghf CAN - 3 31 
4 B ghf CAN ghf 38 42 
",header=F) 
setDT(out) 
names(out) <- c("row","Person","Name","Country","add info","Begin","End") 

Grupowanie powinno odbywać się w sposób następujący: Jeżeli osoba A nie wędrować od 4. miesiąca do 15 miesiąca i podróżując od 16 do 24 miesiąca, grupowałbym kolejną (tj. bez przerwy) aktywność od 4 do 24 miesiąca. Jeśli później osoba A surfowała od 25 miesiąca do 28 miesiąca, dodałabym też to i cała działalność grupy potrwałaby od 4 do 28. Teraz problematyczne są przypadki, w których dochodzi do nakładania się okresów, na przykład osoba A może również łowić ryby od 11 do 31, więc całość będzie wynosić od 4 do 31. Jednakże, jeśli osoba A zrobił coś od 1 do 2, co byłoby odrębnym działaniem (w porównaniu z 1 do 3, co też musiałoby być dodano, ponieważ 3 jest połączone z 4). Mam nadzieję, że było jasne, jeśli nie możesz znaleźć więcej przykładów w powyższym kodzie. Używam datatable, ponieważ mój zbiór danych jest dość duży. Zacząłem od sqldf do tej pory, ale problematyczne jest, jeśli masz tak wiele działań na osobę (powiedzmy 8 lub więcej). Czy można to zrobić w datatable, plyr lub sqldf? Uwaga: również szukam odpowiedzi w SQL, ponieważ mógłbym użyć tego bezpośrednio w sqldf lub spróbować przekonwertować go na inny język. sqldf obsługuje (1) bazę danych SQLite (domyślnie), (2) bazę danych H2 java, (3) baza danych PostgreSQL i (4) sqldf 0.4-0 dalej obsługuje również MySQL.

Edycja: Oto 'czysty' tabel:

W:

Person Name Country add info Begin End 
A  mnb USA  prim  4  12 
A  mnb USA  x   13  15 
A  mnb USA  un  16  25 
A  mnb USA  fdfds  1  2 
B  ghf CAN  sdg  3  27 
B  ghf CAN  hgh  28  29 
B  ghf CAN  y   24  31 
B  ghf CAN  ghf  38  42 

Out:

A  mnb USA  fdfds  1  2 
A  mnb USA  -   4  25 
B  ghf CAN  -   3  31 
B  ghf CAN  ghf  38  42 
+0

Zobacz [Przerwy między pakietami] (http://blogs.solidq.com/en/sqlserver/packing-intervals/) Itzika Ben-Gana. Korzysta z SQL Server, ale najnowsze wersje Postgrera obsługują funkcje tego samego okna co najnowszy SQL Server, więc powinno być proste dostosowanie jego kodu SQL do PostgreSQL. W szczególności zobacz ostatnie rozwiązanie 3 przy użyciu agregatora okien. –

+0

Ty też to sprawdzi – user3032689

+1

Dla rozwiązania R, [ten post] (http://stackoverflow.com/questions/16957293/collapse-intersecting-regions-in-r) wydaje się mieć znaczenie. – Henrik

Odpowiedz

2

Zrobiłem ten jeden, który działał w moich testów i prawie wszystko główne bazy danych powinny normalnie go uruchamiać ... Podkreśliłem moje kolumny ... proszę, zmień nazwy przed testem:

SELECT 
    r1.person_, 
    r1.name_, 
    r1.country_, 
    CASE 
    WHEN max(r2.begin_) = max(r1.begin_) 
    THEN max(r1.info_) ELSE '-' 
    END info_, 
    MAX(r2.begin_) begin_, 
    r1.end_ 
FROM stack_39626781 r1 
INNER JOIN stack_39626781 r2 ON 1=1 
    AND r2.person_ = r1.person_ 
    AND r2.begin_ <= r1.begin_ -- just optimizing... 
LEFT JOIN stack_39626781 r3 ON 1=1 
    AND r3.person_ = r1.person_ 
    -- matches when another range overlaps this range end 
    AND r3.end_ >= r1.end_ + 1 
    AND r3.begin_ <= r1.end_ + 1 
LEFT JOIN stack_39626781 r4 ON 1=1 
    AND r4.person_ = r2.person_ 
    -- matches when another range overlaps this range begin 
    AND r4.end_ >= r2.begin_ - 1 
    AND r4.begin_ <= r2.begin_ - 1 
WHERE 1=1 
    -- get rows 
    -- with no overlaps on end range and 
    -- with no overlaps on begin range 
    AND r3.person_ IS NULL 
    AND r4.person_ IS NULL 
GROUP BY 
    r1.person_, 
    r1.name_, 
    r1.country_, 
    r1.end_ 

To zapytanie jest oparte na fakcie, że dowolny zakres z wyjścia nie ma połączeń/zachodzi na siebie.Powiedzmy, że dla wyjścia z pięciu zakresów, pięć begin s i pięć end s istnieje bez połączeń/nakładek. Wyszukiwanie i kojarzenie ich powinno być łatwiejsze niż generowanie wszystkich połączeń/nakładek. Tak więc to pytanie dotyczy:

  1. Znajdź wszystkie zakresy na osobę bez nakładających się/połączeń na wartości end;
  2. Znajdź wszystkie zakresy na osobę bez zakładek/połączeń na wartości begin;
  3. To są prawidłowe zakresy, więc powiąż je wszystkie, aby znaleźć właściwą parę;
  4. Dla każdego person i end, prawidłowa begin para jest maksymalna jeden dostępny których wartość jest równa lub mniejsza niż ta end ... jest to łatwe, aby potwierdzić tę regułę ... Po pierwsze, nie można mieć begin większa niż end ... także, jeśli masz dwa lub więcej możliwych begin s mniejszych niż end, np. Np., begin1 = koniec - 2 i begin2 = koniec - 5 wybranie mniejsze, ( begin2) powoduje większe, ( begin1) nakładanie tego zakresu.

Mam nadzieję, że to pomaga.

+0

Dzięki. Po prostu patrząc na twój kod, zastanawiam się, czy to działa również dla więcej niż 4 osób? Ponieważ w moim zestawie danych może być 100 osób. Poza tym, który SQL Vers używałeś do tego? Po prostu upewnij się, że mogę zastosować kod. – user3032689

+0

Działa dla dowolnej liczby osób i będzie działać na SQLite, MySql, Postgre, Oracle, MariaDB ... Możesz wypróbować na http://sqlfiddle.com –

+0

Dzięki za dodatkowe wyjaśnienie. Przetestuję to jutro. – user3032689

2

Jeśli pracujesz z SQL Server 2012 lub powyżej, można użyć LGD i LEAD działają, aby zbudować swoją logikę, aby dotrzeć do końcowego pożądanego zestawu danych. Te funkcje są również dostępne w Oracle od czasu Oracle 8i.

Poniżej znajduje się rozwiązanie, które stworzyłem w SQL Server 2012, które powinno ci pomóc. Podane wartości przykładowe są ładowane do tabeli tymczasowej, aby przedstawić to, co określasz jako pierwszy "czysty stół". Używając tych dwóch funkcji, wraz z klauzulą ​​OVER, dotarłem do ostatecznego zestawu danych z poniższym kodem T-SQL. Zostawiłem niektóre z zakomentowanych linii w kodzie, aby pokazać, w jaki sposób udało mi się zbudować kompleksowe rozwiązanie kawałek po kawałku, co odpowiada różnym scenariuszom umieszczonym w instrukcji CASE dla kolumny GapMarker, która działa jak flaga grupująca.

IF OBJECT_ID('tempdb..#MyTable') IS NOT NULL 
 
\t DROP TABLE #MyTable 
 

 
CREATE TABLE #MyTable (
 
\t Person CHAR(1) 
 
\t ,[Name] VARCHAR(3) 
 
\t ,Country VARCHAR(10) 
 
\t ,add_info VARCHAR(10) 
 
\t ,[Begin] INT 
 
\t ,[End] INT 
 
) 
 

 
INSERT INTO #MyTable (Person, Name, Country, add_info, [Begin], [End]) 
 
\t VALUES ('A', 'mnb', 'USA', 'prim', 4, 12), 
 
\t ('A', 'mnb', 'USA', 'x', 13, 15), 
 
\t ('A', 'mnb', 'USA', 'un', 16, 25), 
 
\t ('A', 'mnb', 'USA', 'fdfds', 1, 2), 
 
\t ('B', 'ghf', 'CAN', 'sdg', 3, 27), 
 
\t ('B', 'ghf', 'CAN', 'hgh', 28, 29), 
 
\t ('B', 'ghf', 'CAN', 'y', 24, 31), 
 
\t ('B', 'ghf', 'CAN', 'ghf', 38, 42); 
 

 
WITH CTE 
 
AS 
 
(SELECT 
 
\t \t mt.Person 
 
\t \t ,mt.Name 
 
\t \t ,mt.Country 
 
\t \t ,mt.add_info 
 
\t \t ,mt.[Begin] 
 
\t \t ,mt.[End] 
 
\t \t --,LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [End]) 
 
\t \t --,CASE WHEN [End] + 1 = LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [End]) 
 
\t \t --  --AND LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [End]) = LEAD([End], 1) OVER (PARTITION BY mt.Person ORDER BY [End]) 
 
\t \t -- \t THEN 1 
 
\t \t -- \t ELSE 0 
 
\t \t -- END AS Grp 
 
\t \t --,MARKER = COALESCE(LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [End]), LAG([End], 1) OVER (PARTITION BY mt.Person ORDER BY [End])) 
 
\t \t ,CASE 
 
\t \t \t WHEN mt.[End] + 1 = COALESCE(LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [End]), LAG([End], 1) OVER (PARTITION BY mt.Person ORDER BY [End])) OR 
 
\t \t \t \t 1 + COALESCE(LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [End]), LAG([End], 1) OVER (PARTITION BY mt.Person ORDER BY [End])) = mt.[Begin] OR 
 
\t \t \t \t COALESCE(LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [Begin]), LAG([End], 1) OVER (PARTITION BY mt.Person ORDER BY [Begin])) BETWEEN mt.[Begin] AND mt.[End] OR 
 
\t \t \t \t [End] BETWEEN LAG([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [Begin]) AND LAG([End], 1) OVER (PARTITION BY mt.Person ORDER BY [Begin]) THEN 1 
 
\t \t \t ELSE 0 
 
\t \t END AS GapMarker 
 
\t \t ,InBetween = COALESCE(LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [Begin]), LAG([End], 1) OVER (PARTITION BY mt.Person ORDER BY [Begin])) 
 
\t \t ,EndInBtw = LAG([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [Begin]) 
 
\t \t ,LagEndInBtw = LAG([End], 1) OVER (PARTITION BY mt.Person ORDER BY [Begin]) 
 
\t FROM #MyTable mt 
 
--ORDER BY mt.Person, mt.[Begin] 
 
) 
 
SELECT DISTINCT 
 
\t X.Person 
 
\t ,X.[Name] 
 
\t ,X.Country 
 
\t ,t.add_info 
 
\t ,X.MinBegin 
 
\t ,X.MaxEnd 
 
FROM (SELECT 
 
\t \t c.Person 
 
\t \t ,c.[Name] 
 
\t \t ,c.Country 
 
\t \t ,c.add_info 
 
\t \t ,c.[Begin] 
 
\t \t ,c.[End] 
 
\t \t ,c.GapMarker 
 
\t \t ,c.InBetween 
 
\t \t ,c.EndInBtw 
 
\t \t ,c.LagEndInBtw 
 
\t \t ,MIN(c.[Begin]) OVER (PARTITION BY c.Person, c.GapMarker ORDER BY c.Person) AS MinBegin 
 
\t \t ,MAX(c.[End]) OVER (PARTITION BY c.Person, c.GapMarker ORDER BY c.Person) AS MaxEnd 
 
\t --, CASE WHEN c.[End]+1 = c.MARKER 
 
\t --  OR c.MARKER +1 = c.[Begin] 
 
\t -- THEN 1 
 
\t -- ELSE 0 
 
\t -- END Grp 
 
\t FROM CTE AS c) X 
 
LEFT JOIN #MyTable AS t 
 
\t ON t.[Begin] = X.[MinBegin] 
 
\t \t AND t.[End] = X.[MaxEnd] 
 
\t \t AND t.Person = X.Person 
 
ORDER BY X.Person, X.MinBegin 
 
--ORDER BY Person, [Begin]

A oto zrzut ekranu wyników pasujących do wymaganego ostatecznego zestawu danych:

enter image description here

+0

Ty, wygląda to bardzo dobrze, ale niestety nie mogłem go przetestować. Mam tylko pakiet 'sqldf' dla R, który mówi: sqldf obsługuje (1) bazę danych SQLite (domyślnie), (2) bazę danych H2 java, (3) bazę danych PostgreSQL i (4) sqldf 0.4-0 dalej obsługuje także MySQL. Nie jestem pewien, jak wykonać ten kod SQL Server teraz, ale również. Więc teraz musiałbym przetłumaczyć składnię na jakąś wersję SQL, która obsługuje okienkowanie, prawda? – user3032689

+1

@ user3032689, najnowsze wersje Postgrera obsługują funkcje tego samego okna co SQL Server 2012+. –

+0

OK, ale czy to nie jest ta sama składnia co PostgresSQL? Koniec końców będę musiał przetłumaczyć oba języki, które zakładam. – user3032689