在Mathematica中基于另一个列表的值拆分列表

3
在Mathematica中,我有一个点坐标列表。
size = 50;
points = Table[{RandomInteger[{0, size}], RandomInteger[{0, size}]}, {i, 1, n}];

以及这些点所属的集群索引列表

clusterIndices = {1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1};

什么是将点根据clusterIndices值分成两个单独列表的最简单方法?
编辑: 我想出的解决方案:
pointIndices =
  Map[#[[2]] &,
    GatherBy[MapIndexed[{#1, #2[[1]]} &, clusterIndices], First],
    {2}];
pointsByCluster = Map[Part[points, #] &, pointIndices];

有更好的方法来完成这个吗?

6个回答

5

如@High Performance Mark和@Nicholas Wilson所说,我会建议通过TransposeThread将这两个列表合并。在这种情况下,

In[1]:= Transpose[{clusterIndices, points}]==Thread[{clusterIndices, points}]
Out[1]:= True

有一次,我比较了一下哪个更快,我认为 Thread 稍微快一些。但是,只有在使用非常长的列表时才真正重要。

@High Performance Mark 提出了使用 Select 的好建议。但是,它只允许您一次提取一个聚类。选择聚类1的代码如下:

Select[Transpose[{clusterIndices, points}], #[[1]]==1& ][[All, All, 2]]

既然您似乎想要生成所有的集群,我建议按照以下步骤进行:

GatherBy[Transpose[{clusterIndices, points}], #[[1]]& ][[All, All, 2]]

这个方法的优点是只有一行代码,唯一棘手的部分在于选择正确的结果列表Part。确定需要多少个All项的技巧在于注意到:

Transpose[{clusterIndices, points}][[All,2]]

需要从转置列表中取回点数,因此需要使用第二个All来处理“聚类”列表中的一个额外级别。

值得注意的是,GatherBy中的第二个参数是一个接受一个参数的函数,可以与任何您希望使用的函数交换。因此,它非常有用。但是,如果您想在收集数据时对其进行转换,我建议您查看ReapSow

编辑:ReapSow有些被低估,但功能强大。它们有些令人困惑,但我怀疑GatherBy在内部使用它们实现。例如,

Reap[ Sow[#[[2]], #[[1]] ]& /@ Transpose[{clusterIndices, points}], _, #2& ]

这段代码与我之前的代码做的事情相同,但不需要将索引从点中删除。基本上,Sow会为每个点标记其索引,然后Reap收集所有标记(第二个参数使用_),并仅输出点。个人而言,我使用这个方法替代了GatherBy,并将其编码为一个函数进行加载,如下所示:

SelectEquivalents[x_List,f_:Identity, g_:Identity, h_:(#2&)]:=
   Reap[Sow[g[#],{f[#]}]&/@x, _, h][[2]];

注意:这段代码是对5.x帮助文件中内容的修改。但是,6.0和7.0版本的帮助文件删除了很多有用的示例之一,其中就包括这个示例。

+1 这个回答比 @Mark Fisher 和其他几个回答好多了。我也会加上 @Michael Pilat 的 SplitBy 建议。 - High Performance Mark
非常详细的解释。谢谢! - Max
@Max:不用谢。ReapSow的另一个优点是,你可以sow多个标签,即如果你的数据适合多个类别,你可以将数据分组。为此,请在SelectEquivalents中删除f周围的花括号,并让f返回数据所属的标签列表。 - rcollyer
给那位给这个回答点了踩的人,为什么?告诉对方哪些方面需要改进是基本礼貌。有人愿意承认吗? - rcollyer

5

以下是使用7.0版本中的新SplitBy函数来简洁地完成此操作的方法,速度应该相当快:

SplitBy[Transpose[{points, clusterIndices}], Last][[All, All, 1]]

如果你没有使用7.0版本,你可以这样实现:

Split[Transpose[{points, clusterIndices}], Last[#]==Last[#2]& ][[All, All, 1]]

更新

抱歉,我没有看到您只想要两组的要求,我认为这是聚类而不是拆分。以下是相关代码:

FindClusters[Thread[Rule[clusterIndices, points]]]

一开始我尝试使用SplitBy,但是它无效。试着执行SplitBy[{1, 1, 1, 2, 2, 1, 1, 2, 2 }],你会得到长度为4的{{1, 1, 1}, {2, 2}, {1, 1}, {2, 2}},而我只有两个聚类,需要的是2个。 - Max
你仍然有 clusterIndices = {2,1,...} 的问题。需要进行排序以确保第一个聚合列表表示第一个聚类索引,以保证安全。 - Nicholas Wilson
抱歉,我误解了您想要基于列表将聚类与分割进行区分的意思,实际上您想要在转换点将列表分成较小的聚类。我的回答已经更新以满足这一需求。 - Michael Pilat
或者,您可以使用GatherBy,但是FindClusters可能更快。 - Michael Pilat
+1 对于最易读的答案,尽管收获播种法更有趣。 - Timo
显示剩余3条评论

4
这个怎么样?
points[[
    Flatten[Position[clusterIndices, #]]
    ]] & /@
 Union[clusterIndices]

有趣,是什么让你想到这个的? - Davorak
不知道。我经常使用这种东西(位置列表来提取我想要的内容)。 - Mark Fisher
有趣且与他人建议大不相同。谢谢。 - Max
我怀疑 Union 隐含的排序和列表遍历的次数(通过 PositionPart)会导致这个过程非常低效。然而,对于一个短列表来说,这绝对是 Position 的有趣用法。 - rcollyer

1

我不知道什么是“更好的”,但在函数式语言中,更常见的方式不是添加索引来标记每个元素(您的MapIndexed),而是沿着每个列表运行:

Map[#1[[2]] &, 
 Sort[GatherBy[
   Thread[ {#1, #2} &[clusterIndices, points]],
   #1[[1]] &], #1[[1]][[1]] < #2[[1]][[1]] &], {2}]

大多数在Lisp/ML等语言中成长的人会立即写出Thread函数,这是实现这些语言中的zip思想的方法。

我加入了Sort,因为如果clusterIndices = {2[...,2],1,...},你的实现看起来会遇到麻烦。另一方面,我仍然需要添加一行代码来解决问题,如果clusterIndices有一个3但没有2,则输出索引将是错误的。从你的片段中不清楚你打算如何检索东西。

我认为,如果你用像Haskell这样的语言构建一个简单的CAS作为爱好项目,你会发现列表处理要容易得多,因为其语法比Mathematica更适合函数式列表处理。


将两个列表合并的部分可以简化为Thread[{clusterindices, points}],因为在这种情况下,纯函数部分({#1,#2}) 是多余的按键。 - rcollyer

1
如果我想到更简单的方法,我会在帖子中添加。
Map[#[[1]] &, GatherBy[Thread[{points, clusterIndices}], #[[2]] &], {2}]

1

我的第一步将是执行

Transpose[{clusterIndices, points}]

我的下一步取决于您想要使用它来做什么;我想到的是 Select


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接