知识库

在 Cypher 中处理连胜/连败

在使用 Cypher 进行数据分析时,您可能会遇到需要根据某种“连胜/连败”(streak)来识别或过滤的问题。

例如,对于一个体育图谱,您可能想知道一个球队连续获胜或失败的最大次数。

在这样的查询中,您可能已经将数据排序并放入列表中,但需要弄清楚如何从列表中获取连胜/连败信息。

使用 APOC 将列表分解为连续的连胜/连败

APOC Procedures 提供了丰富的辅助函数和过程,允许您以各种有趣的方式查询和操作集合和映射。

对于这类特定问题,集合过程 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 是一个列表类型,而我们只想要存在的一个值,即我们击败的对手的连胜/连败记录。

© . All rights reserved.