【Excel VBA入門 第25回】配列で処理を高速化しよう!表を一括で取り込む・一括で書き戻す

スポンサーリンク
Excel VBA

イントロダクション

前回(第24回)は、For で表を1行ずつ処理して、条件に合う行を別のシートに転記するマクロを作った。サンプルは10行ほどの小さな表だったので、一瞬で終わったはずである。

しかし、実務の表は、数万行、数十万行になることもある。1行ずつセルを読み書きする方法は、行数が増えると、処理に時間がかかるようになる。セルを読み書きするたびに、VBAとExcel本体の間で、やり取りが発生するためである。

そこで今回は、表を配列に一括で取り込み、配列の上で処理して、結果を一括で書き戻す方法をご紹介しよう。表データの処理編の最終回として、実際に処理時間を測って、違いを確かめてみよう。

  • 第23回:データの最終行を取得する
  • 第24回:表を1行ずつ処理する(条件に合う行を抽出して転記する)
  • 第25回(今回):配列を使って、処理を高速化する

第15回で学んだ配列の知識が、ここで活きてくる。

準備

今回のマクロは、これまでに作った次の部品を使う。
同じ標準モジュールに入っていることを確認しておこう。

部品作った回役割
最終行を取得する(Function)第23回最終行を求める
シートを用意する(Function)第24回指定した名前のシートを作る(あれば中身を消す)
担当者つきサンプルデータを作る(Sub)第24回「売上データ」シートに、10件のサンプルを作る

最初の実験では、第24回で作った10件の「売上データ」シートを使う。
次の実験では、速度の違いが分かるように、10万行の表を用意する。

【注意】実行前・実行後

練習用のブックで試そう

今回のマクロは、次の3つのシートを、自動で作る。すでに同じ名前のシートがある場合は、内容を消してから使うので、必ず練習用のブックで試そう。

  • 大量データ
  • 抽出_1行ずつ
  • 抽出_配列

また、以下で試す「1行ずつ」の処理は、時間がかかることがあり、その間は、Excelが応答しなくなったように見える。終わるまで待とう。
なお、他のブックで作業しているときは、先に保存・終了してから実行しよう。
処理に時間がかかる場合もあることから、パソコンへの負担がご心配なら、大量データを作る の 件数 を、50000 などに小さくして、試してください。
なお、途中で Esc キーなどで止めたときは、後始末が実行されず、画面の更新が止まったままになることがある。その場合は、イミディエイトウィンドウに、Application.ScreenUpdating = True と入力して、Enter を押そう。

また、実験が終わったら、「大量データ」「抽出_1行ずつ」「抽出_配列」の3つのシートを、削除しておこう。10万行のデータが入ったままだと、ブックのファイルの容量が大きくなり、開いたり保存したりするのに、時間がかかるようになる。

配列に一括で取り込む

まず、基本の動きを確かめよう。Range の Value を、Variant 型の変数に代入すると、セル範囲の値が、2次元の配列として一括で取り込まれる。

Sub 配列に取り込む()
    Dim ws As Worksheet
    Dim データ As Variant
    Dim 最終行 As Long
    Dim 行 As Long

    Set ws = ThisWorkbook.Worksheets("売上データ")
    最終行 = 最終行を取得する(ws, 1)

    If 最終行 < 2 Then
        MsgBox "データがありません。", vbExclamation
        Exit Sub
    End If

    データ = ws.Range("A2:D" & 最終行).Value

    Debug.Print "行数:" & UBound(データ, 1) & "、列数:" & UBound(データ, 2)
    For 行 = 1 To UBound(データ, 1)
        Debug.Print データ(行, 2) & ":" & データ(行, 3)
    Next 行
End Sub

実行すると、イミディエイトウィンドウ(Ctrl + G)に、「行数:10、列数:4」に続いて、商品名と売上が10行ぶん表示される。

ポイントは、次の3つである。

  • データ = ws.Range("A2:D" & 最終行).Value
    A2からD列の最終行までを、1回の代入で、配列に取り込んでいる。データ部分だけを取り込むので、見出しの1行目は含めていない
  • データ(行, 列)
    配列の要素は、「何行目の、何列目か」で指定する。ws.Cells(行, 列) と同じ感覚で使える
  • UBound(データ, 1) と UBound(データ, 2)
    配列の行数と列数を調べる。UBound の2つ目の引数が、何番目の次元かを表す(1が行、2が列)

注意したいのは、セルから取り込んだ配列の番号は、1から始まることである。
第15回で扱った Array 関数で作った配列は0から始まるが、セルから取り込んだ配列は、1から始まる。また、最終行が1(データなし)のまま A2:D1 のような範囲を指定すると、意図しない範囲になってしまう。そのため、最初に 最終行 < 2 かどうかを確認している(第19回の事前チェックである)。

速度を比べるため10万行の表を作る

それでは、速度の違いが分かる、大きな表を用意しよう。
10万行ぶんのデータを、配列を使って、一括でシートに書き込むマクロである。
ここでも、配列の「一括で書き戻す」動きが使われている。

Sub 大量データを作る()
    Const 件数 As Long = 100000

    Dim ws As Worksheet
    Dim データ() As Variant
    Dim 商品 As Variant
    Dim 担当 As Variant
    Dim i As Long

    Set ws = シートを用意する("大量データ")

    商品 = Array("りんご", "みかん", "ぶどう", "バナナ", "メロン")
    担当 = Array("田中", "佐藤", "鈴木")

    ' 配列の上で、10万行ぶんのデータを作る
    ReDim データ(1 To 件数, 1 To 4)
    For i = 1 To 件数
        データ(i, 1) = DateSerial(2026, 1, 1) + (i Mod 300)
        データ(i, 2) = 商品(i Mod 5)
        データ(i, 3) = 500 + ((i * 37) Mod 2000)
        データ(i, 4) = 担当(i Mod 3)
    Next i

    ' 見出しと、データを一括で書き込む
    ws.Range("A1:D1").Value = Array("日付", "商品", "売上", "担当")
    ws.Range("A2").Resize(件数, 4).Value = データ
    ws.Range("A2").Resize(件数, 1).NumberFormat = "m/d"
End Sub

「大量データ」というシートが作られ、A1:D100001に、10万件のデータが書き込まれる。実行には、数秒かかることがある。

ReDim データ(1 To 件数, 1 To 4) は、配列の大きさを決める命令である。ws.Range("A2").Resize(件数, 4) は、A2を起点に、件数 行・4列の範囲を作る。書き込み先の範囲の大きさと、配列の大きさを、同じにするのが、一括で書き込むときの基本である。

このデータでは、売上が1,000以上の行は、ちょうど75,000件になる。
後ほどの実験で、確認に使える。

1行ずつ処理する方法

準備ができたので、同じ処理を、2つの方法で作って、時間を比べてみよう。「売上が1,000以上の行を、別シートに抽出する」という処理である。

今回作るのは、次の3つである。実行するのは、最後の 速度を比べる だけで、前の2つは、そこから呼び出される。

  • 抽出_1行ずつ(Function):1行ずつの方法で抽出して、かかった時間を返す
  • 抽出_配列(Function):配列を使う方法で抽出して、かかった時間を返す
  • 速度を比べる(Sub):2つの Function を呼び出して、時間を比べる。実行の入口になる

引数のある Function は、単独では実行できない。3つともコードを貼り付けてから、速度を比べる を実行しよう。

まず、第24回と同じ「1行ずつ」の方法である。処理にかかった時間を返す Function にしている。

Function 抽出_1行ずつ(ByRef 件数 As Long) As Double
    Dim 元 As Worksheet
    Dim 先 As Worksheet
    Dim 最終行 As Long
    Dim 行 As Long
    Dim 書込行 As Long
    Dim 開始 As Double

    Set 元 = ThisWorkbook.Worksheets("大量データ")
    Set 先 = シートを用意する("抽出_1行ずつ")
    最終行 = 最終行を取得する(元, 1)

    先.Range("A1:D1").Value = 元.Range("A1:D1").Value
    書込行 = 2

    開始 = Timer
    For 行 = 2 To 最終行
        If 元.Cells(行, 3).Value >= 1000 Then
            先.Cells(書込行, 1).Value = 元.Cells(行, 1).Value
            先.Cells(書込行, 2).Value = 元.Cells(行, 2).Value
            先.Cells(書込行, 3).Value = 元.Cells(行, 3).Value
            先.Cells(書込行, 4).Value = 元.Cells(行, 4).Value
            書込行 = 書込行 + 1
        End If
    Next 行

    件数 = 書込行 - 2
    抽出_1行ずつ = Timer - 開始
End Function

Timer は、午前0時からの経過秒数を返す関数である。処理の前後で Timer の値を取り、その差を求めれば、処理にかかった時間(秒)が分かる。

件数 は ByRef(参照渡し)で受け取っている。Function は、戻り値を1つしか返せないので、「処理時間」を戻り値にして、「抽出した件数」は、引数を通じて呼び出し元に返している。

配列を使う方法

次に、同じ処理を、配列を使って作る。

Function 抽出_配列(ByRef 件数 As Long) As Double
    Dim 元 As Worksheet
    Dim 先 As Worksheet
    Dim 入力 As Variant
    Dim 出力() As Variant
    Dim 最終行 As Long
    Dim 行 As Long
    Dim 列 As Long
    Dim 出力行 As Long
    Dim 開始 As Double

    Set 元 = ThisWorkbook.Worksheets("大量データ")
    Set 先 = シートを用意する("抽出_配列")
    最終行 = 最終行を取得する(元, 1)

    先.Range("A1:D1").Value = 元.Range("A1:D1").Value

    開始 = Timer

    ' 1. 表を、配列に一括で取り込む
    入力 = 元.Range("A2:D" & 最終行).Value

    ' 2. 条件に合う件数を数えて、出力用の配列の大きさを決める
    件数 = 0
    For 行 = 1 To UBound(入力, 1)
        If 入力(行, 3) >= 1000 Then 件数 = 件数 + 1
    Next 行

    If 件数 > 0 Then
        ReDim 出力(1 To 件数, 1 To 4)

        ' 3. 条件に合う行を、出力用の配列に詰める
        出力行 = 0
        For 行 = 1 To UBound(入力, 1)
            If 入力(行, 3) >= 1000 Then
                出力行 = 出力行 + 1
                For 列 = 1 To 4
                    出力(出力行, 列) = 入力(行, 列)
                Next 列
            End If
        Next 行

        ' 4. 出力用の配列を、一括で書き戻す
        先.Range("A2").Resize(件数, 4).Value = 出力
        先.Range("A2").Resize(件数, 1).NumberFormat = "m/d"
    End If

    抽出_配列 = Timer - 開始
End Function

流れは、次の4ステップである。

  1. 表全体を、入力 という配列に、1回で取り込む
  2. 配列の上で、条件に合う件数を数える
  3. 件数が分かったので、ちょうどの大きさの 出力 配列を用意して、条件に合う行を詰める
  4. 出力 配列を、1回でシートに書き戻す

2番目で、先に件数を数えているのは、出力 配列の大きさを決めるためである。書き込み先の範囲と配列の大きさが一致していないと、意図しない結果になることがある。そのため、いったん件数を数えてから、ちょうどの大きさで用意している。

なお、この Function は、大量データ にデータが入っていることを前提にしている。実務で使うときは、先ほどの 配列に取り込む のように、最終行が2未満(データがない)かどうかを、Range("A2:D" & 最終行) の前に確認しておこう。

「1行ずつの方法」では、セルの読み書きが、10万行ぶん繰り返される。
配列を使う方法では、セルを読み書きするのは、取り込みと書き戻しの、たった2回である。それ以外の処理は、すべて配列の上(VBAの中)で行われる。

実行して時間を比べる

最後に、2つの Function を呼び出して、時間を比べるマクロを作る。画面の更新を止めるのは、どちらの方法でも使われる、定番の高速化である。第18回で学んだ後始末の形も、ここで使っている。

Sub 速度を比べる()
    Dim 件数A As Long
    Dim 件数B As Long
    Dim 時間A As Double
    Dim 時間B As Double

    On Error GoTo エラー処理
    Application.ScreenUpdating = False

    時間A = 抽出_1行ずつ(件数A)
    時間B = 抽出_配列(件数B)

    MsgBox "【1行ずつ】" & 件数A & " 件 " & Format(時間A, "0.00") & " 秒" & vbCrLf & _
           "【配列】  " & 件数B & " 件 " & Format(時間B, "0.00") & " 秒"

後始末:
    Application.ScreenUpdating = True
    Exit Sub

エラー処理:
    MsgBox "処理中にエラーが発生しました。" & vbCrLf & _
           Err.Number & ":" & Err.Description, vbCritical
    Resume 後始末
End Sub

必ず大量データを作る を実行してから、速度を比べる を実行しよう。
メッセージボックスに、次のように、件数と時間が表示される。

  • 件数は、どちらも「75000 件」になる
  • 時間は、パソコンの性能によって変わるが、「1行ずつ」より「配列」のほうが、はるかに短いことが分かる

配列を使うときの注意点

配列は便利だが、いつでも使えばよいわけではない。次の点に注意しよう。

  • 取り込まれるのは、値だけ
    数式のセルは計算結果の値になり、色や罫線などの書式は、配列には入らない。配列に取り込んだ表を、同じ場所に書き戻すと、数式が値に置き換わってしまうので注意しよう。書式も扱いたいときは、Copy などのセル操作が必要である
  • 1つのセルだけの Value は、配列にならない
    Range("A1").Value は、1つの値が返る。配列として扱えるのは、複数のセルの範囲である
  • 処理が単純なら、読みやすさを優先する
    数百行程度の表なら、1行ずつの方法でも、一瞬で終わる。コードも短く、読みやすい。数万行以上の表で、処理に時間がかかるようになったら、配列の出番である

まとめ

今回は、配列を使った高速化を学んだ。

  • 1行ずつセルを読み書きすると、行数が増えたときに、時間がかかる
  • Range の Value を Variant 型の変数に代入すると、セル範囲が2次元の配列として、一括で取り込める
  • セルから取り込んだ配列は、1から始まる。UBound(配列, 1) が行数、UBound(配列, 2) が列数である
  • 配列の上で処理して、Range(...).Resize(行数, 列数).Value = 配列 で、一括で書き戻せる
  • 書き込み先の範囲と配列の大きさは、一致させる
  • Timer で、処理時間を測れる
  • 取り込まれるのは値だけである。数式や書式が必要なときは、セル操作を使う

表データの処理編では、3回かけて、次のことを学んだ。

回テーマ主な内容
第23回最終行の取得End(xlUp)、CurrentRegion、UsedRange
第24回1行ずつ処理For、If、And / Or、別シートへの転記、IsNumeric
第25回高速化配列による一括取り込み・一括書き戻し、Timer

これで、行数が変わる表を、最後まで確実に、しかも速く処理できるようになった。ブック・ファイル操作編と組み合わせれば、「フォルダ内の複数のブックの表を、それぞれ配列で取り込んで、1つにまとめる」といったマクロも作れる。ぜひ、身近な作業で試してみてほしい。

次回からは、新しいテーマに進む予定である。

コメント

タイトルとURLをコピーしました