1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Excelシート内のデータ集合2群の要素の差を出力するVBA

Last updated at Posted at 2018-09-02

内容

Excelシート内で、下記のような調査を行うことが多々あります。

集合A 集合B
This This
column aulumn
will will
be become
⇒集合Aの要素にはcolumnが1回、beが1回多く登場
 集合Bの要素にはautumnが1回、becomeが1回多く登場。
4個だったら目で見て分かりますが、10万行とかあると大変です。

たぶん多くの事務作業者にとっての頻出作業なので、ネットにも方法がたくさん出てきます。ただその多くは、=countif(B:B,A1)とかvlookupなどをセルの横に張り付けて、そのあと下にコピペしていくという、というもので、これだとB:Bから対象があるかを数えるという作業をAデータの存在する数だけ繰り返すので、1000行とかならいけますが、A・Bデータが10万行ずつとかになると、(単純に考えれば10万×10万の計算が行われるため)エクセルが固ったりします。

関数でやるよりもVBAで、辞書型で登場回数をカウント(集合Aの要素は+でカウント、集合Bの要素は-でカウント)、合計が0でないものを出力するとすれば速いはず!と思って作ったのが下記です。50万個ずつでも(差が少なければ普通のPCでも)数秒で終わります。

使い方

使い方は、以下のコードを個人用マクロモジュールにでも貼り付けて、見たい差のデータがあるワークシートをアクティブにした状態でMain_ShowDifferenceBetweenColumnsInActiveSheet()を実行すればOKです。

Option Explicit
Sub Main_ShowDifferenceBetweenColumnsInActiveSheet()
'参照設定:"Microsoft Scripting Runtime"
'重複したデータの値を合計して表示
    Dim myDic As Object 'Dictionary
    Dim i As Long, v As Variant, myStr1 As String, myStr2 As String
    Set myDic = CreateObject("Scripting.Dictionary")
    Dim targetArray1 As Variant, targetArray2 As Variant
    '以下のInputBoxでは、RangeをSetしているのではなく、Range内のValueを取得しているだけであることに注意する。
    targetArray1 = selectSampleByUser("1つめのサンプルを選択してください。列は選択できません。") 'Range(Cells(1, 1), Cells(10, 1))
    targetArray2 = selectSampleByUser("2つめのサンプルを選択してください。列は選択できません。") 'Range(Cells(1, 3), Cells(10, 3))
    Call addValue2Key(myDic, targetArray1, 1)
    Call addValue2Key(myDic, targetArray2, -1)
    Call outputKeys(myDic)
    Set myDic = Nothing
End Sub

Private Function selectSampleByUser(msg As String) As Range
    'InputBoxを表示して、ユーザーから列でないセル選択範囲を指定してもらい、それを返す関数。
    Dim inputRange As Range
    On Error Resume Next
        Set inputRange = Application.InputBox(Prompt:=msg, Type:=8)
    On Error GoTo 0
    If inputRange Is Nothing Then End
    Dim bottomRow As Long
    'このCountで何をやっているかについては以下のサイトがわかりやすいhttp://officetanaka.net/excel/vba/tips/tips111.htm
    bottomRow = inputRange(inputRange.Count).Row
    If bottomRow > 1000000 Then
        MsgBox "ちょっと指定範囲が多すぎると思います"
        End
    End If
    Set selectSampleByUser = inputRange
End Function

Private Sub addValue2Key(ByRef target_dict As Object, ByVal array_range As Variant, value_added As Long)
    'target_dictはDictionary型
    Dim i As Long
    Dim myKey As Variant
    For Each myKey In array_range
        If target_dict.Exists(myKey) Then
            target_dict(myKey) = target_dict(myKey) + value_added
            If target_dict(myKey) = 0 Then
                'ここで不要なKeyをせっせと除くことで、最後に出力する際の検索が速くなる。
                target_dict.Remove myKey
            End If
        Else
            target_dict.Add myKey, value_added
        End If
    Next
End Sub

Private Sub outputKeys(ByRef target_dict As Object) 'Dictionary)
    Dim wbAdded As Workbook
    Set wbAdded = Workbooks.Add ' ワークブックを作成
    Dim i As Long, j As Long, k As Long
    Dim col1_key As Long, col1_num As Long, col2_key As Long, col2_num
    j = 1: k = 1
    col1_key = 1
    col1_num = col1_key + 1
    col2_key = col1_key + 2
    col2_num = col1_key + 3
    With wbAdded.Worksheets(1)
        .Cells(j, col1_key).Value = "指定1に多く登場"
        .Cells(j, col1_num).Value = "登場回数差"
        j = j + 1
        .Cells(k, col2_key).Value = "指定2に多く登場"
        .Cells(k, col2_num).Value = "登場回数差"
        k = k + 1
        For i = 1 To target_dict.Count
            If target_dict.Items()(i - 1) > 0 Then
            '細かいことをいうと.Items,.Keysは配列を返すメソッドであり()が必要だが、参照設定をすればこの()は付けなくて済むようだ。
            '参照設定なしで済ませたいので今回は()が必要。
                .Cells(j, col1_key).Value = target_dict.Keys()(i - 1)
                .Cells(j, col1_num).Value = target_dict.Items()(i - 1)
                j = j + 1
            ElseIf target_dict.Items()(i - 1) < 0 Then
                .Cells(k, col2_key).Value = target_dict.Keys()(i - 1)
                .Cells(k, col2_num).Value = target_dict.Items()(i - 1) * (-1)
                k = k + 1
            End If
        Next i
    End With
End Sub

性能についてテスト

  • 要素が各1万個程度でも、2つの集合で異なる要素が多いとかなり時間がかかる。
    • 試しに「=RANDBETWEEN(0,10000)」をA1セル~B10000セルにコピー&値貼り付けした後、A列とB列で比べてみたら4秒くらいかかった。
  • 要素が各10万個程度でも、2つの集合で異なる要素が少ないと速い。
    • A1セル~A100000セルに「=RANDBETWEEN(0,10000)」の値貼り付けを作成、B列にA列と同じ数値を張り付けてテストしたところ2秒程度で終了。ちなみにA列とB列で順番を逆順にしてみても時間は変わらなかった。

課題

  • コードの最後で、セル1つずつに張り付けていく部分があるが、これは速度の観点から望ましくない。おそらく、一度配列に格納して、最後にセルに1度に貼り付けるのが望ましいと思う。だが辞書のKeyやItemを、正負の条件を満たすものに絞って配列化するスマートな方法がぱっと思いつかず諦めた。
  • 現状のコードで、異なる要素が多いと時間がかかるのは、Itemsをindexで指定して繰り返し出力していく部分に時間がかかるためだと思う。うまいことやればもっと高速化できる気がするし、したいのはやまやまなのだが、そのためにはDictionaryの実装がどうなっているのかとか、もうちょっと詳しく調べる必要がある気がする。
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?