PairWise 匹配数百万条记录

Posted

技术标签:

【中文标题】PairWise 匹配数百万条记录【英文标题】:PairWise matching millions of records 【发布时间】:2013-11-21 09:14:01 【问题描述】:

我手头有一个算法问题。为了方便地解释这个问题,我将使用一个简单的类比。 我有一个输入文件

Country,Exports
Austrailia,Sheep
US, Apple
Austrialia,Beef

最终目标: 我必须找到这对国家之间的共同产品,所以

"Austrailia,New Zealand":"apple","sheep
"Austrialia,US":"apple"
"New Zealand","US":"apple","milk"

流程:

我读入输入并将其存储在 TreeMap > 列表中,由于许多重复,字符串被保留。 本质上,我是按国家/地区汇总的。 其中 Key 是国家/地区,Values 是其出口。

"austrailia":"apple","sheep","koalas"
"new zealand":"apple","sheep","milk"
"US":"apple","beef","milk"

我有大约 1200 个键(国家),值(出口)的总数是 8000 万。 我对每个键的所有值进行排序:

"austrailia":"apple","sheep","koalas" -- > "austrailia":"apple","koalas","sheep"

这很快,因为只有 1200 个列表要排序。

for(k1:keys)
   for(k2:keys)
        if(k1.compareTo(k2) <0) //Dont want to double compare
    List<String> intersectList = intersectList_func(k1's exports,k2's exports);
        countriespair.put(k1,k2,intersectList)

这个代码块需要很长时间。我意识到它是 O(n2) 和大约 1200*1200 的比较。因此,到现在运行了将近 3 个小时.. 有什么办法,我可以加快或优化它。 明智的算法是最好的选择,或者还有其他技术可以考虑。

编辑由于两个 List 都是预先排序的,因此 intersectList 是 O(n),其中 n 是 floor(listOne.length,listTwo.length) 的长度,而不是 O(n2),如下所述

private static List<String> intersectList(List<String> listOne,List<String> listTwo)
        int i=0,j=0;
        List<String> listResult = new LinkedList<String>(); 
        while(i!=listOne.size() && j!=listTwo.size())
            int compareVal = listOne.get(i).compareTo(listTwo.get(j));
            if(compareVal==0)
                listResult.add(listOne.get(i));
                i++;j++;               
            else if(compareVal < 0) i++;
            else if (compareVal >0) j++;   
        
        return listResult;
    

11 月 22 日更新 我当前的实现仍然运行了将近 18 个小时。 :|

11 月 25 日更新 我已经按照 Vikram 和其他一些人的建议运行了新的实现。本周五一直在运行。 我的问题是,按出口而非国家/地区分组如何节省计算复杂性。我发现复杂性是一样的。正如 Groo 提到的,我发现第二部分的复杂性是 O(E*C^2),其中 E 是出口,C 是国家/地区。

【问题讨论】:

使用 SQL 数据库和查询将是一个可能的解决方案。 @prog_guy 提供您的输入文件以测试我的代码 【参考方案1】:

这可以在一个语句中使用 SQL 作为自连接来完成:

测试数据。首先创建一个测试数据集:

Lines <- "Country,Exports
Austrailia,Sheep
Austrailia,Apple
New Zealand,Apple
New Zealand,Sheep
New Zealand,Milk
US,Apple
US,Milk
"
DF <- read.csv(text = Lines, as.is = TRUE)

sqldf 现在我们有DF 发出这个命令:

library(sqldf)
sqldf("select a.Country, b.Country, group_concat(Exports) Exports
   from DF a, DF b using (Exports) 
   where a.Country < b.Country
   group by a.Country, b.Country
")

给出这个输出:

      Country     Country     Exports
1  Austrailia New Zealand Sheep,Apple
2  Austrailia          US       Apple
3 New Zealand          US  Apple,Milk

带索引如果速度太慢,请在 Country 列中添加索引(并确保不要忘记 main. 部分:

sqldf(c("create index idx on DF(Country)",
   "select a.Country, b.Country, group_concat(Exports) Exports
   from main.DF a, main.DF b using (Exports) 
   where a.Country < b.Country
   group by a.Country, b.Country
"))

如果内存不足,则添加 dbname = tempfile() sqldf 参数,以便它使用磁盘。

【讨论】:

不错的解决方案。不是真正的数据库人,所以几个qn。 ...... 1)我记得使用(col_name)连接只在​​两个列相等时才有效,它是如何通过交集工作的?............ .2 ) group by 的最后一行是什么?通过删除它,只返回一行。到目前为止,我已经使用 group by 来汇总结果,反之亦然。 …………我还没有用整个数据集对它进行基准测试。 (1) 是的,我们通过Exports 加入,并且在幸存的国家/地区对中必须是相同的。 (2) 我们正在对group by 定义的组执行group_concat 聚合。【参考方案2】:

存储类似于以下数据结构的内容:-(以下是伪代码)

ValuesSet =
apple = "Austrailia","New Zealand"..
sheep = "Austrailia","New Zealand"..  



for k in ValuesSet 
    for k1 in k.values() 
       for k2 in k.values()   
           if(k1<k2)
              Set(k1,k2).add(k)

时间复杂度:O(具有相似产品的不同对的数量)

注意:我可能错了,但我认为你不能降低这个时间复杂度

以下是针对您的问题的 java 实现:-

public class PairMatching 

    HashMap Country;
    ArrayList CountNames;
    HashMap ProdtoIndex;
    ArrayList ProdtoCount;
    ArrayList ProdNames;
    ArrayList[][] Pairs;

    int products=0;
    int countries=0;


    public void readfile(String filename) 
        try 
            BufferedReader br = new BufferedReader(new FileReader(new File(filename)));
            String line;
            CountNames = new ArrayList();
            Country = new HashMap<String,Integer>();
            ProdtoIndex = new HashMap<String,Integer>();
            ProdtoCount = new ArrayList<ArrayList>();
            ProdNames = new ArrayList();
            products = countries = 0;
            while((line=br.readLine())!=null) 
                String[] s = line.split(",");
                s[0] = s[0].trim();
                s[1] = s[1].trim();
                int k;
                if(!Country.containsKey(s[0])) 
                    CountNames.add(s[0]);
                    Country.put(s[0],countries);
                    k = countries;
                    countries++;
                 
                else 
                    k =(Integer) Country.get(s[0]);
                
                if(!ProdtoIndex.containsKey(s[1])) 
                    ProdNames.add(s[1]);
                    ArrayList n = new ArrayList();
                    ProdtoIndex.put(s[1],products);
                    n.add(k);
                    ProdtoCount.add(n);
                    products++;
                
                else 
                    int ind =(Integer)ProdtoIndex.get(s[1]);
                    ArrayList c =(ArrayList) ProdtoCount.get(ind);
                    c.add(k);
                
            
            System.out.println(CountNames);
            System.out.println(ProdtoCount);
            System.out.println(ProdNames);

         catch (FileNotFoundException ex) 
            Logger.getLogger(PairMatching.class.getName()).log(Level.SEVERE, null, ex);
         catch (IOException ex) 
            Logger.getLogger(PairMatching.class.getName()).log(Level.SEVERE, null, ex);
        


    

    void FindPairs() 
        Pairs = new ArrayList[countries][countries];
        for(int i=0;i<ProdNames.size();i++) 
            ArrayList curr = (ArrayList)ProdtoCount.get(i);
            for(int j=0;j<curr.size();j++) 
                for(int k=j+1;k<curr.size();k++) 
                    int u =(Integer)curr.get(j);
                    int v = (Integer)curr.get(k);
                    //System.out.println(u+","+v);
                    if(Pairs[u][v]==null) 
                        if(Pairs[v][u]!=null)
                            Pairs[v][u].add(i);
                        else 
                            Pairs[u][v] = new ArrayList();
                            Pairs[u][v].add(i);
                        
                    
                    else Pairs[u][v].add(i);
                
            
        
        for(int i=0;i<countries;i++) 
            for(int j=0;j<countries;j++) 
                if(Pairs[i][j]==null)
                    continue;
                ArrayList a = Pairs[i][j];
                System.out.print("\n"+CountNames.get(i)+","+CountNames.get(j)+" : ");
                for(int k=0;k<a.size();k++) 
                    System.out.print(ProdNames.get((Integer)a.get(k))+" ");
                
            
        
    

    public static void main(String[] args) 
       PairMatching pm = new PairMatching();
       pm.readfile("Input data/BigData.txt");
       pm.FindPairs();


    


【讨论】:

for 循环非常具有误导性。对于 k1 中的 k ( shld 是 k.values() 中的 k1 ,同样适用于 k2 。【参考方案3】:

[更新] 与 OP 的原始算法相比,此处介绍的算法不应提高时间复杂度。 两种算法具有相同的渐近复杂度,并且遍历排序列表(如 OP 所做的那样)通常应该性能比使用哈希表更好。

您需要按product 而非country 对项目进行分组,以便能够快速获取属于某个产品的所有国家/地区。

这将是伪代码:

inputList contains a list of pairs country, product

// group by product 
prepare mapA (product) => (list_of_countries)
for each country, product in inputList
      
   if mapA does not contain (product)
      create a new empty (list_of_countries) 
      and add it to mapA with (product) as key

   add this (country) to the (list_of_countries)


// now group by country_pair  
prepare mapB (country_pair) => (list_of_products)       
for each product, list_of_countries in mapA
   
   for each pair countryA, countryB in list_of_countries
   
      if mapB does not countain country_pair countryA, countryB
         create a new empty (list_of_products) 
         and add it to mapB with country_pair countryA, countryB as key

      add this (product) to the (list_of_products)
   

如果您的输入列表长度为 N,并且您有 C 个不同的国家和 P 个不同的产品,那么该算法的运行时间应该是第一部分的 O(N) 和第二部分的 O(P*C^2)。由于您的最终列表需要将国家/地区对映射到产品列表,我认为您无论如何都不会失去P*C^2 的复杂性。

我不会用 Java 编写太多代码,所以我添加了一个 C# 示例,我相信您可以轻松移植:

// mapA maps each product to a list of countries
var mapA = new Dictionary<string, List<string>>();
foreach (var t in inputList)

    List<string> countries = null;
    if (!mapA.TryGetValue(t.Product, out countries))
    
        countries = new List<string>();
        mapA[t.Product] = countries;
    
    countries.Add(t.Country);


// note (this is very important):
// CountryPair tuple must have value-type comparison semantics, 
// i.e. you need to ensure that two CountryPairs are compared
// by value to allow hashing (mapping) to work correctly, in O(1).

// In C# you can also simply use a Tuple<string,string> to 
// represent a pair of countries (which implements this correctly),
// but I used a custom class to emphasize the algorithm

// mapB maps each CountryPair to a list of products
var mapB = new Dictionary<CountryPair, List<string>>();
foreach (var kvp in mapA)

    var product = kvp.Key;
    var countries = kvp.Value;

    for (int i = 0; i < countries.Count; i++)
    
        for (int j = i + 1; j < countries.Count; j++)
        
            var pair = CountryPair.Create(countries[i], countries[j]);
            List<string> productsForCountryPair = null;
            if (!mapB.TryGetValue(pair, out productsForCountryPair))
            
                productsForCountryPair = new List<string>();
                mapB[pair] = productsForCountryPair;
            
            productsForCountryPair.Add(product);
        *
    

【讨论】:

我运行了这个新的实现,我按出口而不是国家来分组。它从周五晚上开始运行。我的问题是,这个新实现如何降低计算复杂度。我发现复杂性几乎相同。列表之间的交集之间的唯一区别。如果我错了,请纠正我。 这个算法最重要的部分是正确地实现CountryPair相等/散列功能。这意味着您必须override equals/getHashCode 并确保您的哈希函数不会提供太多冲突。你也可以使用任何以这种方式实现相等的元组类(例如javatuples)。如果你的哈希函数写得不好,那么你将丢失O(1) 查找国家对地图,时间会变短。 @prog_guy:我还通过一些复杂性分析编辑了我的答案,所以你可以检查我的数字是否正确。 你有大约 65,000 个不同的产品 (80M / 1.2k = 66.6k),我有大约 370 万个不同的产品,而不是 66K。我仍然不同意为什么我的 intersect 方法是 O(n2),我仍然认为它是 O(n) .. 对于 CountryPair 类,它有 2 个字符串,我现在使用通用 Objects.hash(country1,country2) .. 也会看看 javatuples。 @prog_guy:是的,你是对的,在最坏的情况下 intersect 迭代两个列表直到结束,即O(2n) = O(n)。这确实使两种复杂性相等(除非您考虑到排序部分,O(n log n),它执行速度很快),很抱歉我第一次没有更好地分析您的代码。您的代码甚至可能表现出更好的引用局部性,但是您可以通过使用 ArrayList 而不是 LinkedList 来减少分配。【参考方案4】:

这是使用Map Reduce 的一个很好的例子。

在您的地图阶段,您只需收集属于每个国家/地区的所有出口。 然后,reducer 对产品进行排序(产品属于同一个国家,因为 mapper)

您将受益于可分布到集群中的分布式并行算法。

【讨论】:

如果他对于 8000 万条记录只有 1200 个键,那么 MapReduce 是该问题的最差选择(实际上它取决于所使用的分组算法和实现)。还有80米。根本没有那么大,您需要一个分布式系统。 与keys相反的大量记录,实际上是map reduce的一个问题。适当的分组设计将提高程序的性能。此外,硬件(即内核数量)是关键因素。但总的来说,这比 prog_guy 给出的初始解决方案更好。【参考方案5】:

您实际上花费了 O(n^2 * 1 个相交所需的时间)。

让我们看看我们是否可以改善相交时间。我们可以为每个存储相应产品的国家维护地图,因此您有 n 个国家/地区的 n 个哈希地图。只需要遍历所有产品一次即可进行初始化。如果您想快速查找,请将地图维护为:

    HashMap<String,HashMap<String,Boolean>> countryMap = new HashMap<String, HashMap<String,Boolean>>();

现在,如果您想查找国家 str1 和 str2 的常见产品,请执行以下操作:

    HashMap<String,Boolean> map1 = countryMap.get("str1");
    HashMap<String,Boolean> map2 = countryMap.get("str2");

    ArrayList<String > common = new ArrayList<String>();
    Iterator it = map1.entrySet().iterator();
    while (it.hasNext()) 
        Map.Entry<String,Boolean> pairs = (Map.Entry)it.next();

        //Add to common if it is there in other map
        if(map2.containsKey(pairs.getKey()))
            common.add(pairs.getKey());
    

因此,假设哈希映射查找实现为 O(1)(我猜它是 java 的 log k),如果一个映射中有 k 个条目,总共将是 O(n^2 * k)。

【讨论】:

复杂度是 O(n),是的,Java 中的查找是 O(1)。您的循环结构可以简化为map1.keySet().retainAll(map2.keySet()) @ThomasJungblut n 个国家 (nC2) 有 O(n^2) 种组合。如果每个国家/地区有 O(k) 个产品,那么总体复杂性将是 O(n^2 * k),因为正如您提到的,在 java 中查找是 O(1)。【参考方案6】:

在必要时使用哈希图来加快速度:

1) 浏览数据并创建一个地图,其中包含与该项目相关的国家/地区列表。所以例如绵羊:澳大利亚、美国、英国、新西兰……

2) 创建一个哈希图,其中包含每对国家/地区的键和(最初)一个空列表作为值。

3) 对于每个项目,检索与其关联的国家/地区列表,并为该列表中的每对国家/地区,将该项目添加到在步骤 (2) 中为该对创建的列表中。

4) 现在输出每对国家/地区的更新列表。

最大的成本在步骤 (3) 和 (4) 中,这两个成本都与产出的产量成线性关系,所以我认为这离最优值不远了。

【讨论】:

和我的文字回答一样 原来如此——我刚看的时候看到一堆嵌套的 for 循环,没再看多远。我认为我在声称它是最佳的时提供了更多细节。 没关系 :),对于多人支持它的答案总是有好处的。

以上是关于PairWise 匹配数百万条记录的主要内容,如果未能解决你的问题,请参考以下文章

MySQL 最长前缀匹配 100 万条记录与 3000 种可能性

使用 Postgres 全文搜索搜索完全匹配的最佳方法是啥?

在数据表中显示数百万条记录

删除数百万条记录 oracle [关闭]

多连接数百万条记录的优化查询需要建议

数百万条记录的增量更新,索引与连接