在 Cypher 中处理连续记录(streaks)
在使用 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。这样仍然可以使用共同的分隔值来划分连续纪录,但每个纪录元素现在可以更丰富。
话虽如此,我们需要调整计算 longestStreak 的方式,因为 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() 在 streakLength 上的简洁性非常好。
幸运的是,APOC 提供了一个相对较新的聚合函数,可以帮助我们保持这种简洁性,避免自行排序和收集。
apoc.coll.maxItems()(还有 apoc.coll.minItems())可以在取某个值的最大值的同时,保留与该最大值关联的项目。
把它加入查询中吧。
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 是列表类型,而我们只需取其中的单一值,即我们击败的对手列表。
此页面有帮助吗?