【问题标题】:Find rows repeated in no particular order and show result sorted by number of repetitions查找不按特定顺序重复的行并显示按重复次数排序的结果
【发布时间】:2014-09-24 01:18:50
【问题描述】:

我能够使用 Python 解决这个问题,但我还需要在 Excel 本身中实现该解决方案,以便我可以使用图形轻松地表示结果。

鉴于此表:

b   a   c
c   a   b 
a   c   b
a   c
a   c   d
b   c   a
d   c   a

我想获得一个列表,该列表按表中某行重复(无特定顺序)的次数排序。

  • 因此这将被视为重复行:“a b c”、“c b a”、“a c b”
  • 但这不会:“a b c”、“b c”、“b”、“a b”、“a c”

所以,我正在寻找的输出是这样的:

1st place: "b+a+c" found 4 times
2nd place: "a+c+d" found 2 twice
3rd place: "a+c" found once

输出必须说“b+a+c”,即使它还计算“a+b+c”、“c+b+a”等等......因为“b+a+c”是所有其他后续重复中的第一个。

谁能告诉我解决问题的正确方法?

【问题讨论】:

  • 为什么不从python导入输出?
  • 如果你已经在 python 中解决了这个问题,ExcelPython 可能会有用。它是 python 函数的 COM 包装器,允许您从 VBA 调用它们。
  • 我找到了一个解决方案并添加了另一个答案,希望它对你有用。
  • 谢谢我去看看ExcelPython,我不知道它存在!

标签: excel excel-formula vlookup worksheet-function vba


【解决方案1】:

我这样做的方法是使用字典来遍历列表并计算行数。关键是行本身,所以我可以使用字典的Dictionary.Exists(Key) 方法来查看我是否已经遇到过该行。与每个键关联的值将是一个整数,每次我再次遇到同一行时都会递增。

解析列表后,我将迭代字典以将它们的键和值输出到 excel 中的列。 最后,我会在输出结果的范围内使用 sort 按频率对它们进行排序。

这是很简单的事情,但是您需要引用 Microsoft Scripting Runtime 才能使用字典对象(参见此处,例如 http://www.techbookreport.com/tutorials/vba_dictionary.html)。

希望这会有所帮助。

更新

既然你说你可以在 vba 中尝试这个方法。当我第一次使用CollectionDictionary 对象时,我想我会添加一些总是让我绊倒的东西。遍历条目时,迭代变量必须是Variant。我习惯于必须声明与我正在迭代的数据相同类型的迭代变量,但这会在 vba 中给你一个错误。

【讨论】:

  • 只是想澄清一下,既然您只要求提供方法,我在这里不提供任何示例代码。
  • 非常感谢,这与我在 Python 中所做的非常相似。我从来没有在 Excel 中编写过代码,但我会看看那个链接,看看我是否可以管理它。谢谢!
  • 别担心,希望你能完成它。如果你需要任何帮助,你知道在哪里问:)
【解决方案2】:

我建议你用另一种方法来解决这个问题。

您可以将 a b c d 转移到 1 2 4 8(二进制为 01 10 100 1000)。

a+b+c = a+c+b =... = 7 (111)
a+c = c+a = 5 (101)

所以你可以在excel中使用总和值进行分组。

单个字符转数字的函数很简单:

A B C POWER(2,CODE(A2) - 97)  POWER(2,CODE(A2) - 97)  POWER(2,CODE(A2) - 97)  SUM(D2:F2)
-+-+-+-----------------------+-----------------------+-----------------------+----------
b|a|c|2                      |1                      |4                      |7
c|a|b|4                      |1                      |2                      |7
a|c|b|1                      |4                      |2                      |7
a|c| |1                      |4                      |0                      |5
a|c|d|1                      |4                      |8                      |13
b|c|a|2                      |4                      |1                      |7
d|c|a|8                      |4                      |1                      |13

希望这种方法可以帮助您找到解决问题的方法。

【讨论】:

  • 真是个问题。但是我的方法可以扩展以适应这种情况。第一个可以使用POWER(2,CODE('a') - 97),第二个可以使用POWER(2,CODE('a')-97)*POWER(2,16),第三个可以使用POWER(2,CODE('a')-97)*POWER(2,32)
  • 上一条评论中提到的方法不正确。它将 a+a 与 a 分开,但也将 a+b+a 与 a+a+b 分开。我的方法只是使用哈希映射比较之类的原理。所以 {a}+{a} 与 {a} 相同,{a}+{b}+{a} = {a}+{a}+{b} = {a}+{b}。
  • 这是解决这个问题的好方法。谢谢你,今天学到了一些新东西。 +1
  • 谢谢,我会试试的。问题是我的行中有大约 200 个不同的短语而不是 a、b、c、d(这 200 个或多或少地重复,如示例中的 a、b、c、d)。如果我设法为这些短语中的每一个分配一个数字,它仍然可以工作......还是会弄乱二进制文件?请原谅这个问题,但我现在正在工作,还不能测试这个解决方案。
【解决方案3】:

我会使用一个类模块和一个集合对象。类模块由两个数组和一个计数器组成。第一个数组是原始顺序中的行;第二个数组是按排序顺序排列的行。排序后的顺序将用作集合对象的键。如果尝试在 Key 已经存在的地方添加集合对象,则会导致错误。捕获错误并将计数器加一。

然后对于结果,您将从“原始”数组中检索原始条目;和柜台。在柜台上排序,你就有了结果。

这是完成上述操作的 VBA 代码示例。

首先,插入一个 Class 模块并将其重命名为 RowEntries

Option Explicit
Private pOriginal() As Variant
Private pSorted() As Variant
Private pCount As Long

Public Property Get Original() As Variant
    Original = pOriginal
End Property
Public Property Let Original(Value As Variant)
    pOriginal = Value
End Property

Public Property Get Sorted() As Variant
    Sorted = pSorted
End Property
Public Property Let Sorted(Value As Variant)
    pSorted = Value
End Property

Public Property Get Count() As Long
    Count = pCount
End Property
Public Property Let Count(Value As Long)
    pCount = Value
End Property

然后插入一个常规模块。此代码假定您的源数据是 A1 附近的 CurrentRegion;结果将向右移动几列。这些算法很容易改变。

Option Explicit
Option Compare Text  'To make comparison case insensitive, if you want
Sub RankRows()
    Dim V As Variant, VtoSort As Variant
    Dim vRes() As Variant
    Dim cRowEntries As RowEntries
    Dim colRowEntries As Collection
    Dim sKey As String, S As String
    Dim I As Long
    Dim rSrc As Range, rRes As Range  'Location for Results

Set rSrc = Range("A1").CurrentRegion
Set rRes = rSrc.Offset(columnoffset:=rSrc.Columns.Count + 3).Resize(1, 2)

V = rSrc

Set colRowEntries = New Collection
On Error Resume Next
For I = 1 To UBound(V)
    Set cRowEntries = New RowEntries
    With cRowEntries
        .Original = WorksheetFunction.Index(V, I, 0)
        VtoSort = .Original
        Quick_Sort VtoSort, LBound(VtoSort), UBound(VtoSort)
        .Sorted = VtoSort
        .Count = 1
        sKey = CStr(Join(.Sorted, ", "))
        colRowEntries.Add cRowEntries, sKey
        If Err.Number <> 0 Then
            Err.Clear
            With colRowEntries(sKey)
                .Count = .Count + 1
            End With
        End If
    End With
Next I
On Error GoTo 0

'populate results array
ReDim vRes(1 To colRowEntries.Count, 1 To 2)
For I = 1 To colRowEntries.Count
    With colRowEntries(I)
        vRes(I, 1) = Join(.Original, "+")

            'remove trailing delimiters
            Do While Right(vRes(I, 1), 1) = "+"
                vRes(I, 1) = Left(vRes(I, 1), Len(vRes(I, 1)) - 1)
            Loop

        vRes(I, 2) = .Count
    End With
Next I

Set rRes = rRes.Resize(rowsize:=UBound(vRes), columnsize:=UBound(vRes, 2))
With rRes
    .EntireColumn.Clear
    .Value = vRes
    .Sort key1:=rRes.Columns(2), order1:=xlDescending, Header:=xlNo
End With

V = rRes
ReDim vRes(1 To UBound(V), 1 To 1)

For I = 1 To UBound(V)
    Select Case V(I, 2)
        Case 1
            S = "once"
        Case 2
            S = "twice"
        Case Else
            S = V(I, 2) & " times"
    End Select
    vRes(I, 1) = OrdinalNum(I) & " place: """ & V(I, 1) & """ found " & S

Next I

rRes.EntireColumn.Clear
rRes.Resize(columnsize:=1) = vRes
rRes.EntireColumn.AutoFit

End Sub


Sub Quick_Sort(ByRef SortArray As Variant, ByVal first As Long, ByVal last As Long)
Dim Low As Long, High As Long
Dim Temp As Variant, List_Separator As Variant
Low = first
High = last
List_Separator = SortArray((first + last) / 2)
Do
    Do While (SortArray(Low) < List_Separator)
        Low = Low + 1
    Loop
    Do While (SortArray(High) > List_Separator)
        High = High - 1
    Loop
    If (Low <= High) Then
        Temp = SortArray(Low)
        SortArray(Low) = SortArray(High)
        SortArray(High) = Temp
        Low = Low + 1
        High = High - 1
    End If
Loop While (Low <= High)
If (first < High) Then Quick_Sort SortArray, first, High
If (Low < last) Then Quick_Sort SortArray, Low, last
End Sub

Function OrdinalNum(num) As String
Dim Suffix As String

OrdinalNum = num
If Not IsNumeric(num) Then Exit Function
If num <> Int(num) Then Exit Function

Select Case num Mod 10
    Case Is = 1
        Suffix = "st"
    Case Is = 2
        Suffix = "nd"
    Case Is = 3
        Suffix = "rd"
    Case Else
        Suffix = "th"
End Select

Select Case num Mod 100
    Case 11 To 19
        Suffix = "th"
End Select

OrdinalNum = Format(num, "#,##0") & Suffix
End Function

输出将与您在上面的请求中显示的一样。但可以轻松修改:

【讨论】:

  • 嗨,我试过了,效果很好。我认为这个答案比其他答案更好地解决了这个问题,主要是因为它可以轻松修改,考虑到所有情况并且似乎可以广泛重复使用。它处理所有类型的情况,而且由于它不是公式,因此修改以测试不同的配置要容易得多。非常感谢!
  • @Manu 不客气。我发现使用 Classes 使一些代码更易于理解和维护。
【解决方案4】:

几乎只有公式的解决方案,假设数据位于标记为 ColumnsA:C 的 D2 中:

=VLOOKUP(A2,weight,2,0)+IFNA(VLOOKUP(B2,weight,2,0),)+IFNA(VLOOKUP(C2,weight,2,0),)  

复制下来以适应,其中weight(图像中的绿色)是查找表的命名范围(按照@Jaugar Chang 建议的线构建)。在 E2 中并复制下来以适应:

 =IF(COUNTIF(D$2:D2,D2)=1,COUNTIF(D:D,D2),"")  

在 G1 中:

=ROW()&MID("thstndrdthstndrdth",MATCH(IF(MOD(ROW(),100)>29,MOD(ROW(),10)+20,MOD(ROW(),100)),{0,1,2,3,4,21,22,23,24},1)*2-1,2)&" place: """&INDIRECT("A"&MATCH(H1,E:E,0))&"+"&INDIRECT("B"&MATCH(H1,E:E,0))&"+"&INDIRECT("C"&MATCH(H1,E:E,0))&""" found"  

上半年:

=LARGE(E:E,ROW())  

在 I1 中:

=IF(H1>2,"times",IF(H1=1,"","twice"))

最后三个中的每一个都复制下来,直到缺少一条错误消息。

H 列格式:

[=1] "once";General

输出以黄色突出显示:

在此示例中,+ 存在盈余,++ 可能存在盈余。

【讨论】:

  • 非常感谢!正如我在另一条评论中所说,我必须用短语而不是字母来实现这一点,但这种方法对我来说似乎更容易理解。我会尝试反馈,谢谢!
【解决方案5】:

这是我使用数组操作的版本,然后是一些范围操作。

Edit1:我已阅读 pnut 关于仅处理 b 的评论。顺便说一句,这不会处理 a+a

Sub Test()
    Dim arr, unq
    Dim orng As Range, rng As Range, srng As Range
    Dim i As Long, k As Long
    Dim check As Boolean: check = False
    Dim freq As String
    '~~> pass range data to array
    Set orng = Sheet1.Range("A1", _
        Sheet1.Range("A" & Sheet1.Rows.Count).End(xlUp))
    For Each rng In orng
        If Not IsArray(arr) Then
            arr = Array(RngToArr(rng.Resize(, 3)))
        Else
            ReDim Preserve arr(UBound(arr) + 1)
            arr(UBound(arr)) = RngToArr(rng.Resize(, 3))
        End If
    Next
    '~~> pass unique combination and count to another array
    For i = LBound(arr) To UBound(arr)
        If IsEmpty(unq) Then
            ReDim unq(1 To 2, 1 To 1)
            unq(1, 1) = arr(i)
            unq(2, 1) = unq(2, 1) + 1
        Else
            For k = LBound(unq, 2) To UBound(unq, 2)
                If CompArr(arr(i), unq(1, k)) Then
                    check = False
                    unq(2, k) = unq(2, k) + 1
                    Exit For
                Else
                    check = True
                End If
            Next
            If check Then
                ReDim Preserve unq(1 To 2, 1 To UBound(unq, 2) + 1)
                unq(1, UBound(unq, 2)) = arr(i)
                unq(2, UBound(unq, 2)) = unq(2, UBound(unq, 2)) + 1
            End If
        End If
    Next
    '~~> Transpose and tidy up the array
    ReDim tally(1 To UBound(unq, 2), 1 To 2)
    For i = LBound(unq, 2) To UBound(unq, 2)
        tally(i, 1) = Join$(unq(1, i), "+")
        tally(i, 2) = unq(2, i)
    Next
    '~~> sort in worksheet, easier than sorting array
    With Sheet1
        Set srng = .Range("E1:F" & UBound(tally, 1))
        srng = tally
        .Sort.SortFields.Clear
        .Sort.SortFields.Add Key:=srng.Offset(0, 1).Resize(, 1), _
            SortOn:=xlSortOnValues, Order:=xlDescending, _
            DataOption:=xlSortNormal
        With .Sort
            .SetRange srng
            .Header = xlGuess
            .Orientation = xlTopToBottom
            .SortMethod = xlPinYin
            .Apply
        End With
    End With
    '~~> do some manipulation to make it closer to what you want
    For Each rng In srng.Offset(0, 1).Resize(, 1)
        Select Case rng.Value
        Case 1: freq = "found once"
        Case 2: freq = "found twice"
        Case Else: freq = "found " & rng.Value & " times"
        End Select
        rng.Value = freq
    Next
End Sub

Private Function CompArr(list1, list2) As Boolean
    Dim j As Long: CompArr = True
    For j = LBound(list1) To UBound(list1)
        With Application
            If IsError(.Match(list1(j), list2, 0)) _
                Then CompArr = False
        End With
    Next
End Function

Private Function RngToArr(r As Range) As Variant
    Dim c As Range, a
    For Each c In r
        If Len(c.Value) <> 0 Then
            If Not IsArray(a) Then
                a = Array(c.Value)
            Else
                ReDim Preserve a(UBound(a) + 1)
                a(UBound(a)) = c.Value
            End If
        End If
    Next
    RngToArr = a
End Function

结果:

不完全是您想要的方式,我无法提出如何动态设置第一名、第二名等
另外,我没有深入研究加号(+)。如果有空格,结果可能是 +b+c,或 a+c+ 或 a++c。
无论如何,HTH。

【讨论】:

  • 非常感谢您的建议,正如您所说,它必须针对“第一名”和“+”号进行一些调整,但这毕竟在问题中并不太重要.我选择的答案似乎与这个非常相似。再次感谢!
【解决方案6】:

这个问题很有趣。这是一个很好的示例,用于展示如何使用数学来提供更简单的解决方案。

我不得不添加另一个答案,因为我意识到找到三个单词的重复组合与从零点计算 distance in three-space 相同 - 只需给每个单词一个不同的数字。而这个答案可以解决之前Pnuts提到的a+a问题。

与我上一个答案不同,如果三个成员中有200个短语和组合,计算出的最大数字是120000 (POWER(200,2)*3),我上一个答案是1.60694E+60 (POWER(2,200) )。我的最后一个答案可能会在逻辑上解决问题,但无法在 Excel 或许多编程语言中实现。它使用 permutations 解决方案来解决 combinations 问题。

这里是三空间距离的解决方案,简单易扩展。

  1. 将每个单词映射到不同的数字。 (VLOOKUP 是一种方法,你可能有其他方法。)结果数不需要连续,只需要彼此不同,最大数应小于 SQRT(POWER(2,32)/3)) .
  2. 使用 G1 中的公式计算距离。
  3. Group 和 Count 使用 G 列。(您可以在其他答案中找到方法。)
  4. 注意:我使用 '_' 替换空格单元格来映射空格的数字,因此您可以使 a_a 等于 aa_(第 4 行和第 5 行)。任何选择都应该有一个空格数字。

任何改进此答案的建议将不胜感激。

【讨论】:

  • 嗨!再一次感谢你。这种方法现在对我来说更有意义,虽然当你必须为 200 个短语分配不同的数字时它看起来有点复杂:如果你添加 1 个短语,那么你可能会重新计算所有内容。再次感谢您的帮助,我会尝试一下,看看是否可以适应我的问题。
  • 如果您添加一个新词组,所有旧词组组合的距离将保持不变。只有新词组的组合才需要计算。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-21
  • 1970-01-01
  • 1970-01-01
  • 2010-10-27
相关资源
最近更新 更多