分组连续范围
Group consecutive ranges
我有一个包含很多行的数据表,我想有条件地对两列进行分组,即开始和结束。这些列代表相关人员正在做某事的某个月份。下面是一些示例数据(你可以用R读入,如果你不使用R,可以找下面的纯表格):
# 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")
分组应按如下方式进行:如果 A 从第 4 个月到第 15 个月徒步旅行,从第 16 个月到第 24 个月旅行,我将连续(即不间断)activity 个月分组4 到 24 个月。如果之后 A 人从 25 个月到 28 个月冲浪,我还要加上这个,整个组 activity 会持续 4 到 28。
现在有问题的是有重叠时期的情况,例如 A 可能也在 11 到 31 钓鱼,所以整个事情会变成 4 到 31。但是,如果 A 从 1 到 2 做了某事,那将是一个单独的activity(与 1 到 3 相比,也必须添加,因为 3 连接到 4)。我希望这很清楚,如果没有,您可以在上面的代码中找到更多示例。
我正在使用数据表,因为我的数据集非常大。到目前为止,我已经开始使用 sqldf,但如果每个人的活动太多(比如 8 个或更多),就会出现问题。
这可以在数据表、plyr 或 sqldf 中完成吗?
请注意:我也在 SQL 中寻找答案,因为我可以直接在 sqldf 中使用它或尝试将其转换为另一种语言。 sqldf 支持 (1) SQLite 后端数据库(默认),(2) H2 java 数据库,(3) PostgreSQL 数据库和 (4) sqldf 0.4-0以后也支持 MySQL.
编辑:这是 'pure' 表:
在:
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
输出:
A mnb USA fdfds 1 2
A mnb USA - 4 25
B ghf CAN - 3 31
B ghf CAN ghf 38 42
如果您使用的是 SQL Server 2012 或更高版本,您可以使用 LAG 和 LEAD 函数来构建您的逻辑以获得最终所需的数据集。我相信,自 Oracle 8i 以来,这些功能在 Oracle 中也可用。
以下是我在 SQL Server 2012 中创建的解决方案,应该对您有所帮助。您提供的示例值已加载到临时 table 中,以表示您所说的第一个 "pure table." 使用这两个函数以及 OVER 子句,我使用以下 T- SQL 下面的代码。我在代码中留下了一些注释掉的行,以展示我是如何逐个构建整体解决方案的,这说明了放置在用作分组标志的 GapMarker 列的 CASE 语句中的各种场景。
IF OBJECT_ID('tempdb..#MyTable') IS NOT NULL
DROP TABLE #MyTable
CREATE TABLE #MyTable (
Person CHAR(1)
,[Name] VARCHAR(3)
,Country VARCHAR(10)
,add_info VARCHAR(10)
,[Begin] INT
,[End] INT
)
INSERT INTO #MyTable (Person, Name, Country, add_info, [Begin], [End])
VALUES ('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);
WITH CTE
AS
(SELECT
mt.Person
,mt.Name
,mt.Country
,mt.add_info
,mt.[Begin]
,mt.[End]
--,LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [End])
--,CASE WHEN [End] + 1 = LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [End])
-- --AND LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [End]) = LEAD([End], 1) OVER (PARTITION BY mt.Person ORDER BY [End])
-- THEN 1
-- ELSE 0
-- END AS Grp
--,MARKER = COALESCE(LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [End]), LAG([End], 1) OVER (PARTITION BY mt.Person ORDER BY [End]))
,CASE
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
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
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
[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
ELSE 0
END AS GapMarker
,InBetween = COALESCE(LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [Begin]), LAG([End], 1) OVER (PARTITION BY mt.Person ORDER BY [Begin]))
,EndInBtw = LAG([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [Begin])
,LagEndInBtw = LAG([End], 1) OVER (PARTITION BY mt.Person ORDER BY [Begin])
FROM #MyTable mt
--ORDER BY mt.Person, mt.[Begin]
)
SELECT DISTINCT
X.Person
,X.[Name]
,X.Country
,t.add_info
,X.MinBegin
,X.MaxEnd
FROM (SELECT
c.Person
,c.[Name]
,c.Country
,c.add_info
,c.[Begin]
,c.[End]
,c.GapMarker
,c.InBetween
,c.EndInBtw
,c.LagEndInBtw
,MIN(c.[Begin]) OVER (PARTITION BY c.Person, c.GapMarker ORDER BY c.Person) AS MinBegin
,MAX(c.[End]) OVER (PARTITION BY c.Person, c.GapMarker ORDER BY c.Person) AS MaxEnd
--, CASE WHEN c.[End]+1 = c.MARKER
-- OR c.MARKER +1 = c.[Begin]
-- THEN 1
-- ELSE 0
-- END Grp
FROM CTE AS c) X
LEFT JOIN #MyTable AS t
ON t.[Begin] = X.[MinBegin]
AND t.[End] = X.[MaxEnd]
AND t.Person = X.Person
ORDER BY X.Person, X.MinBegin
--ORDER BY Person, [Begin]
下面是与您所需的最终数据集匹配的结果的屏幕截图:
我做了这个在我的测试中有效的,几乎所有的主要数据库通常都应该 运行 它...我强调了我的专栏...请在测试前更改名称:
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_
此查询基于以下事实:输出的任何范围都没有 connections/overlaps。可以说,对于五个范围的输出,存在五个 begin
和五个 end
,没有 connections/overlaps。查找并关联它们应该比生成所有 connections/overlaps 更容易。所以,这个查询的作用是:
- 查找
end
值中没有 overlaps/connections 的每个人的所有范围;
- 查找
begin
值中没有 overlaps/connections 的每个人的所有范围;
- 这些是有效范围,所以将它们全部关联起来以找到正确的对;
- 对于每个
person
和 end
,正确的 begin
对是可用的最大一对,其值等于或小于此 end
...这很容易验证此规则...首先,begin
不能大于 end
...另外,如果您有两个或更多可能的 begin
小于 end
,即。例如,begin1 = end - 2 和 begin2 = end - 5,选择较小的 (begin2) 使较大的 (begin1) 与该范围重叠。
希望对您有所帮助。
我有一个包含很多行的数据表,我想有条件地对两列进行分组,即开始和结束。这些列代表相关人员正在做某事的某个月份。下面是一些示例数据(你可以用R读入,如果你不使用R,可以找下面的纯表格):
# 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")
分组应按如下方式进行:如果 A 从第 4 个月到第 15 个月徒步旅行,从第 16 个月到第 24 个月旅行,我将连续(即不间断)activity 个月分组4 到 24 个月。如果之后 A 人从 25 个月到 28 个月冲浪,我还要加上这个,整个组 activity 会持续 4 到 28。 现在有问题的是有重叠时期的情况,例如 A 可能也在 11 到 31 钓鱼,所以整个事情会变成 4 到 31。但是,如果 A 从 1 到 2 做了某事,那将是一个单独的activity(与 1 到 3 相比,也必须添加,因为 3 连接到 4)。我希望这很清楚,如果没有,您可以在上面的代码中找到更多示例。 我正在使用数据表,因为我的数据集非常大。到目前为止,我已经开始使用 sqldf,但如果每个人的活动太多(比如 8 个或更多),就会出现问题。 这可以在数据表、plyr 或 sqldf 中完成吗? 请注意:我也在 SQL 中寻找答案,因为我可以直接在 sqldf 中使用它或尝试将其转换为另一种语言。 sqldf 支持 (1) SQLite 后端数据库(默认),(2) H2 java 数据库,(3) PostgreSQL 数据库和 (4) sqldf 0.4-0以后也支持 MySQL.
编辑:这是 'pure' 表:
在:
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
输出:
A mnb USA fdfds 1 2
A mnb USA - 4 25
B ghf CAN - 3 31
B ghf CAN ghf 38 42
如果您使用的是 SQL Server 2012 或更高版本,您可以使用 LAG 和 LEAD 函数来构建您的逻辑以获得最终所需的数据集。我相信,自 Oracle 8i 以来,这些功能在 Oracle 中也可用。
以下是我在 SQL Server 2012 中创建的解决方案,应该对您有所帮助。您提供的示例值已加载到临时 table 中,以表示您所说的第一个 "pure table." 使用这两个函数以及 OVER 子句,我使用以下 T- SQL 下面的代码。我在代码中留下了一些注释掉的行,以展示我是如何逐个构建整体解决方案的,这说明了放置在用作分组标志的 GapMarker 列的 CASE 语句中的各种场景。
IF OBJECT_ID('tempdb..#MyTable') IS NOT NULL
DROP TABLE #MyTable
CREATE TABLE #MyTable (
Person CHAR(1)
,[Name] VARCHAR(3)
,Country VARCHAR(10)
,add_info VARCHAR(10)
,[Begin] INT
,[End] INT
)
INSERT INTO #MyTable (Person, Name, Country, add_info, [Begin], [End])
VALUES ('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);
WITH CTE
AS
(SELECT
mt.Person
,mt.Name
,mt.Country
,mt.add_info
,mt.[Begin]
,mt.[End]
--,LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [End])
--,CASE WHEN [End] + 1 = LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [End])
-- --AND LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [End]) = LEAD([End], 1) OVER (PARTITION BY mt.Person ORDER BY [End])
-- THEN 1
-- ELSE 0
-- END AS Grp
--,MARKER = COALESCE(LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [End]), LAG([End], 1) OVER (PARTITION BY mt.Person ORDER BY [End]))
,CASE
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
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
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
[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
ELSE 0
END AS GapMarker
,InBetween = COALESCE(LEAD([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [Begin]), LAG([End], 1) OVER (PARTITION BY mt.Person ORDER BY [Begin]))
,EndInBtw = LAG([Begin], 1) OVER (PARTITION BY mt.Person ORDER BY [Begin])
,LagEndInBtw = LAG([End], 1) OVER (PARTITION BY mt.Person ORDER BY [Begin])
FROM #MyTable mt
--ORDER BY mt.Person, mt.[Begin]
)
SELECT DISTINCT
X.Person
,X.[Name]
,X.Country
,t.add_info
,X.MinBegin
,X.MaxEnd
FROM (SELECT
c.Person
,c.[Name]
,c.Country
,c.add_info
,c.[Begin]
,c.[End]
,c.GapMarker
,c.InBetween
,c.EndInBtw
,c.LagEndInBtw
,MIN(c.[Begin]) OVER (PARTITION BY c.Person, c.GapMarker ORDER BY c.Person) AS MinBegin
,MAX(c.[End]) OVER (PARTITION BY c.Person, c.GapMarker ORDER BY c.Person) AS MaxEnd
--, CASE WHEN c.[End]+1 = c.MARKER
-- OR c.MARKER +1 = c.[Begin]
-- THEN 1
-- ELSE 0
-- END Grp
FROM CTE AS c) X
LEFT JOIN #MyTable AS t
ON t.[Begin] = X.[MinBegin]
AND t.[End] = X.[MaxEnd]
AND t.Person = X.Person
ORDER BY X.Person, X.MinBegin
--ORDER BY Person, [Begin]
下面是与您所需的最终数据集匹配的结果的屏幕截图:
我做了这个在我的测试中有效的,几乎所有的主要数据库通常都应该 运行 它...我强调了我的专栏...请在测试前更改名称:
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_
此查询基于以下事实:输出的任何范围都没有 connections/overlaps。可以说,对于五个范围的输出,存在五个 begin
和五个 end
,没有 connections/overlaps。查找并关联它们应该比生成所有 connections/overlaps 更容易。所以,这个查询的作用是:
- 查找
end
值中没有 overlaps/connections 的每个人的所有范围; - 查找
begin
值中没有 overlaps/connections 的每个人的所有范围; - 这些是有效范围,所以将它们全部关联起来以找到正确的对;
- 对于每个
person
和end
,正确的begin
对是可用的最大一对,其值等于或小于此end
...这很容易验证此规则...首先,begin
不能大于end
...另外,如果您有两个或更多可能的begin
小于end
,即。例如,begin1 = end - 2 和 begin2 = end - 5,选择较小的 (begin2) 使较大的 (begin1) 与该范围重叠。
希望对您有所帮助。