在 Cypher 中使用连续序列
在使用 Cypher 进行数据分析时,您可能会遇到需要根据某种连续序列进行识别或过滤的问题。
例如,对于一个体育图谱,您可能想知道一支球队的最大连续获胜或失利次数。
在这种查询中,您可能已经将数据排序并列出,但您需要弄清楚如何从列表中获取连续序列信息。
使用 APOC 将列表分解成连续序列
APOC 过程提供了丰富的辅助函数和过程,使您可以以各种有趣的方式查询和操作集合和映射。
对于这种特定类型的问题,集合过程 apoc.coll.split()
将提供获取连续序列数据的最快捷和最简单的方法。
此过程将列表作为输入以及分隔符值,并在分隔符值周围进行拆分以提供子列表。
例如,我们将使用一个布尔值的字面列表来表示胜利 (true) 与失败 (false),然后围绕失败进行拆分以获取连续胜利的列表
WITH [true, false, true, false, true, true, true, true, false, false, false, true, true] as games
CALL apoc.coll.split(games, false) YIELD value
RETURN value
输出如下所示
╒═════════════════════╕ │"value" │ ╞═════════════════════╡ │[true] │ ├─────────────────────┤ │[true] │ ├─────────────────────┤ │[true,true,true,true]│ ├─────────────────────┤ │[true,true] │ └─────────────────────┘
我们可以改为过滤以获取最大获胜连胜。
WITH [true, false, true, false, true, true, true, true, false, false, false, true, true] as games
CALL apoc.coll.split(games, false) YIELD value as winStreak
RETURN max(size(winStreak)) as longestWinStreak
这给了我们 4 的最长获胜连胜。
一个更复杂的示例
虽然实际的图数据和查询通常并不那么简单,但我们通常可以在查询中对其进行简化。
让我们使用这样的图
(:Team {name:string})-[:PLAYED {won:boolean}]->(:Game {date:date})
这是一个您可以自己测试的精简示例数据集
CREATE (p:Team{name:'Paris St-Germain'}) ,
(d:Team{name:'Dijon'}),
(b:Team{name:'Bordeaux'}),
(a:Team{name:'Amiens SC'}),
(o:Team{name:'Olympique Lyonnais'}),
(n:Team{name:'Nantes'}),
(mp:Team{name:'Montpellier'}),
(l:Team{name:'Lille'}),
(mo:Team{name:'Monaco'}),
(se:Team{name:'Saint-Etienne'})
CREATE (p)-[:PLAYED {won:true }]->(:Game {date:date('2020-02-29')})<-[:PLAYED {won: false}]-(d),
(p)-[:PLAYED {won:true }]->(:Game {date:date('2020-02-23')})<-[:PLAYED {won: false}]-(b),
(p)-[:PLAYED {won:false }]->(:Game {date:date('2020-02-15')})<-[:PLAYED {won: true}]-(a),
(p)-[:PLAYED {won:true }]->(:Game {date:date('2020-09-02')})<-[:PLAYED {won: false}]-(o),
(p)-[:PLAYED {won:true }]->(:Game {date:date('2020-04-02')})<-[:PLAYED {won: false}]-(n),
(p)-[:PLAYED {won:true }]->(:Game {date:date('2020-01-02')})<-[:PLAYED {won: false}]-(mp),
(p)-[:PLAYED {won:true }]->(:Game {date:date('2020-01-26')})<-[:PLAYED {won: false}]-(l),
(p)-[:PLAYED {won:true }]->(:Game {date:date('2020-01-15')})<-[:PLAYED {won: false}]-(mo),
(p)-[:PLAYED {won:false }]->(:Game {date:date('2020-12-01')})<-[:PLAYED {won: true}]-(a),
(p)-[:PLAYED {won:true }]->(:Game {date:date('2020-12-21')})<-[:PLAYED {won: false}]-(se)
此数据集以巴黎圣日耳曼为中心,它没有关于其他球队之间比赛的数据。
我们可以使用与之前简单示例中相同的方法来计算每个球队的最长连续获胜连胜,并相应地对输出进行排序和限制
MATCH (team:Team)-[r:PLAYED]->(game:Game)
WITH team, r, game
ORDER BY game.date ASC
WITH team, collect(r.won) as results
CALL apoc.coll.split(results, false) YIELD value as winStreak
WITH team, max(size(winStreak)) as longestStreak
RETURN team.name as teamName, longestStreak
ORDER BY longestStreak DESC
LIMIT 3
我们的结果是
╒══════════════════╤═══════════════╕ │"teamName" │"longestStreak"│ ╞══════════════════╪═══════════════╡ │"Paris St-Germain"│4 │ ├──────────────────┼───────────────┤ │"Amiens SC" │2 │ └──────────────────┴───────────────┘
我们在这里只看到两个结果,因为在我们的数据集中,其他球队都没有赢得任何比赛,因此没有要报告的连胜。
如果我们也想要比赛数据怎么办?
虽然这使我们能够根据最长的连胜次数获得前 3 名球队,但在这一过程中我们确实丢失了比赛数据。如果我们想知道他们在该单一最长连胜中的每一场比赛中击败了哪些球队呢?
我们可以通过巧妙地使用 CASE 来保留这些数据。与其仅使用 collect(r.won) as results
,我们可以使用 CASE 在球队获胜的情况下投影一些自定义数据,但在球队输球时仅输出 false
。这仍然允许我们使用一个公共值来拆分以查找连续序列,但连续序列的每个元素现在都像我们需要的那样丰富。
也就是说,我们需要调整我们计算最长连胜的方式,因为否则 max()
函数将导致我们丢失我们最终仍然想要的连胜数据。
这是一个应该可以解决问题的修改后的查询
MATCH (team:Team)-[r:PLAYED]->(game:Game)<-[:PLAYED]-(opponent)
WITH team, r, game, opponent
ORDER BY game.date ASC
WITH team, collect(CASE WHEN r.won THEN opponent ELSE false END) as results
CALL apoc.coll.split(results, false) YIELD value as winStreak
WITH team, winStreak, size(winStreak) as streakLength
ORDER BY streakLength DESC
WITH team, collect(winStreak)[0] as streak, max(streakLength) as longestStreak
WITH team, longestStreak, streak
ORDER BY longestStreak DESC
LIMIT 3
RETURN team.name as teamName, longestStreak, [opponent IN streak | opponent.name] as beat
以及查询结果
╒══════════════════╤═══════════════╤══════════════════════════════════════════════════╕ │"teamName" │"longestStreak"│"beat" │ ╞══════════════════╪═══════════════╪══════════════════════════════════════════════════╡ │"Paris St-Germain"│4 │["Bordeaux","Dijon","Nantes","Olympique Lyonnais"]│ ├──────────────────┼───────────────┼──────────────────────────────────────────────────┤ │"Amiens SC" │2 │["Paris St-Germain","Paris St-Germain"] │ └──────────────────┴───────────────┴──────────────────────────────────────────────────┘
请注意在胜利时使用 CASE 对所面对的对手进行自定义投影
collect(CASE WHEN r.won THEN opponent ELSE false END) as results
由于我们需要保留连续序列数据,因此我们必须进行排序,通过收集并仅保留列表开头的连续序列来选择按长度排列的顶级连续序列。
最后,我们将属性投影留到最后,在我们已将结果限制为前 3 名球队(根据其最长的连胜次数)之后,这样我们就可以避免对将被过滤掉的数据进行属性访问。
最后一个简化辅助函数
在查询中间添加我们自己的排序并获取集合的顶部是一件很麻烦的事情。当我们只需要对连续序列长度使用 max()
时,我们所拥有的简单性是很好。
幸运的是,有一个相对较新的 APOC 聚合函数可以帮助我们保持这种简单性,并避免进行我们自己的排序和收集。
apoc.coll.maxItems()
(还有一个 apoc.coll.minItems()
)允许我们获取某个值的 max,但保留与该最大值关联的项。
让我们将其添加到查询中
MATCH (team:Team)-[r:PLAYED]->(game:Game)<-[:PLAYED]-(opponent)
WITH team, r, game, opponent
ORDER BY game.date ASC
WITH team, collect(CASE WHEN r.won THEN opponent ELSE false END) as results
CALL apoc.coll.split(results, false) YIELD value as winStreak
WITH team, apoc.agg.maxItems(winStreak, size(winStreak), 1) as longestStreakData
WITH team, longestStreakData.items[0] as streak, longestStreakData.value as longestStreak
ORDER BY longestStreak DESC
LIMIT 3
RETURN team.name as teamName, longestStreak, [opponent IN streak | opponent.name] as beat
结果与之前相同。
maxItems()
聚合函数调用在这里
WITH team, apoc.agg.maxItems(winStreak, size(winStreak), 1) as longestStreakData
这将获取项、值(我们希望从中获取最大值)以及可选地对具有相同值的项的数量进行限制。一个球队可能有多个相同长度的获胜连胜,但在我们的例子中,我们只对找到的第一个连胜感兴趣,因此我们将限制为每个球队一个连胜,并忽略任何其他连胜。
请注意,我们仍然需要在下一行获取列表的头部
longestStreakData.items[0] as streak
这是因为正如我们刚才提到的,该函数能够获取所有(或可选地限制数量的)共享相同最大值的项(其他相同长度的连胜),因此结果中的 items
是列表类型,我们只想要单个值,即我们击败的对手的连胜。
此页面是否有帮助?