[[20211010110740]] 『マクロで行列の条件に一致する値の取得』(ココア) ページの最後に飛ぶ

[ 初めての方へ | 一覧(最新更新順) | 全文検索 | 過去ログ ]

 

『マクロで行列の条件に一致する値の取得』(ココア)

下記のような事をマクロでやりたいのですが、行き詰ってしまい教えて下さい。

<元データシート>

    A     B     C     D     E     
 1 
 2 コード 種目	4月   5月   6月
 3 A01    A     10    20    10
 4 A01    B     5     10    5
 5 A01    C     10    20    10
 6 A02	  A	20    20    10
 7 A02    B     10    10    10
 8 A02    C     5     10    10
 9 A03    A	8     10    8
 10 A03	  B	2      5    6
 11 A03   C	8     20    6
 12 A01   A     10    20    10
 13 A01   B	5     10    5
 14 A01   C	10    20    10

<貼り付け先シート>

    A     B     
 1  B	
 2  5月	
 3	
 4  A01	  10
 5  A02	  10
 6  A03	  5

やりたい事は、元データシートのコード列&種目列と2行目の月が、貼り付け先シートのA4〜A6セルのコード 及び、A1セルの種目 及び A2セルの月が一致した場合、B4〜B6セルにその結果をかき出したいです。
元データシートの12〜14行目は3〜5行目と全く同じものが入っていますがこのように重複している場合もあります(ただし、全く同じ値が入っている)

これをマクロでやりたいのですが、関数を埋め込む方式なら下記で出来たのですが、For〜Nextや、Dictionaryを使って出来ないか試行錯誤していますがうまくいきません。教えて下さい。

<関数埋め込み版>

Sub 行列取得1()

    Dim ws1 As Worksheet
    Dim ws2 As Worksheet
    Set ws1 = Worksheets("貼り付け先")
    Set ws2 = Worksheets("元データ")

     ws1.Range("B4:B6").Formula = "=INDEX(元データ!$A$2:$E$14,MATCH(貼り付け先!$A4&貼り付け先!$A$1,INDEX(元データ!$A$2:$A$14&元データ!$B$2:$B$14,),0),MATCH(貼り付け先!$A$2,元データ!$A$2:$E$2,0))"

End Sub

<For〜Next版>←エラーが出ます。
Sub 行列取得()

    Dim ws1 As Worksheet
    Dim ws2 As Worksheet
    Set ws1 = Worksheets("貼り付け先")
    Set ws2 = Worksheets("元データ")

    Dim i As Long
    Dim ii As Long
    Dim n As Long
    Dim TargetRow As Long
    Dim TargetCol As Long

    '最終行の取得
    Dim LastRow As Long
    LastRow = ws2.Cells(Rows.Count, 1).End(xlUp).Row
    Dim LastRow2 As Long
    LastRow2 = ws1.Cells(Rows.Count, 1).End(xlUp).Row

'まずコードと種目行を取得する
For i = 3 To LastRow

    For ii = 4 To LastRow2
        If ws2.Cells(i, 1) = ws1.Cells(ii, 1) Then
            If ws2.Cells(i, 2) = ws1.Cells(1, 1) Then
                TargetRow = i '一致した行番号を取得する
                Exit For
            End If
       End If
    Next ii
Next i

    '最終列の取得
    Dim LastColumn As Long
    LastColumn = ws2.Cells(2, Columns.Count).End(xlLeft).Row

'次に月の列を取得する
For i = 3 To LastColumn

    If ws2.Cells(2, i) = ws1.Cells(2, 1) Then
        TargetCol = i '一致した列番号を取得する
         Exit For
    End If
Next i

'結果を出力する
For ii = 4 To LastRow2

    Cells(ii, 2) = Cells(TargetRow, TargetCol).Value
Next ii

End Sub

↑↑
下記でエラーになります。

    LastColumn = ws2.Cells(2, Columns.Count).End(xlToLeft).columun

でもその前のループの仕方も色々違う気がするのですが。。
ご教授頂ければと思います。

また、このような場合はDictionary等でも可能なのでしょうか。
併せて教えて頂けますとうれしいです。

< 使用 Excel:Excel2010、使用 OS:unknown >


 AdvancedFilterを用いて同様の形に出来るかもしれません。
 検索値が無い等のエラー処理はしていませんので、あくまでもご参考程度に。

 Sub sample()
    With Worksheets("貼り付け先")
        .Range("XFD1:XFD2").Value = WorksheetFunction.Transpose(Array("種目", .Range("A1")))
        .Range("A4:B4").Value = Array("コード", .Range("A2"))
        Worksheets("元データ").Cells(2, 1).CurrentRegion.AdvancedFilter _
            Action:=xlFilterCopy, CriteriaRange:=.Range("XFD1:XFD2"), CopyToRange:=.Range("A4:B4"), Unique:=True
        .Range("XFD1").CurrentRegion.Clear
        .Rows(4).Delete
    End With
 End Sub

 ※すみません。一部修正 12:57
(豆腐) 2021/10/10(日) 12:46

豆腐さん
ありがとうございます。なるほど、そのようなアプローチがあるのですね。
やってみたのですが、すみません今回説明用に例題を端折ってしまいましたが、実際は貼り付け先に元データには登場しないコードもあったりするので、そうなるとこのやり方では貼り付け先のリストからそのコードが消えてしまうようです。。今後別の時にこのやり方活用させて頂きます。ありがとうございます。

私が最初に提示した「For〜Next版」の正しい書き方をご教授頂ける方がいらっしゃいましたら、
引き続き宜しくお願いいたします。
(ココア) 2021/10/10(日) 13:20


 Match とか Filter などの機能を使わずに For〜Next でということですね。

 まず、コードを書く時に、読みやすいコードになるように意識しましょう。

 例えば、ws1 ws2 というような名前だと何のシートなのか分かりづらいですね。

 私の感覚だと元データから貼り付け先に転記ということなので、ws1が元、ws2が先という気がしますが、
 質問のコードでは逆になってますね。
そして、astRow2 が ws1 の最終行になってます。行は2なのにシートが1というのも混乱の元です。

 例えば、下記のような変数名にすれば紛れがないですね。
(一例ですので、このへんはうまく工夫してください。)

 元データ   wsMoto
 貼り付け先 wsSaki

 元データ最終行   lastRowMoto
 貼り付け先最終行 lastRowSaki

 次に  For〜Next ですが、
検索のループと出力のループが別になってますが、
出力は複数あるので、別にしたら、一つのデータしか出力できません。
TargetRow, TargetColにはそれぞれ一つの値しか格納できないので、
最後に検索された値が入っているだけですので。

 For〜Next で検索して、一致したらその時に出力して、ループを抜ける、
というような処理にします。

 Sub 行列取得()
    Dim wsMoto As Worksheet
    Dim wsSaki As Worksheet
    Set wsMoto = Worksheets("元データ")
    Set wsSaki = Worksheets("貼り付け先")
    Dim i As Long
    Dim ii As Long

    '最終行の取得
    Dim LastRowMoto As Long
    LastRowMoto = wsMoto.Cells(Rows.Count, 1).End(xlUp).Row
    Dim LastRowSaki As Long
    LastRowSaki = wsSaki.Cells(Rows.Count, 1).End(xlUp).Row

    '検索月の列番号を取得する
    Dim TargetCol As Long
    For i = 3 To wsMoto.Cells(2, Columns.Count).End(xlToLeft).Column
        If wsMoto.Cells(2, i) = wsSaki.Cells(2, 1) Then
            TargetCol = i    '一致した列番号を取得する
            Exit For
        End If
    Next i

    Dim Syumoku
    Syumoku = wsSaki.Cells(1, 1) '種目を取得

    Dim Code As String
    '貼り付け先の走査
    For i = 4 To LastRowSaki 
        Code = wsSaki.Cells(i, 1) 'コードを取得
        '元データを操作
        For ii = 3 To LastRowMoto
            'コードと種目が一致したら
            If Code = wsMoto.Cells(ii, 1) And Syumoku = wsMoto.Cells(ii, 2) Then
                wsSaki.Cells(i, 2) = wsMoto.Cells(ii, TargetCol) '該当月の列の値を出力
                Exit For
            End If
        Next ii
    Next i

 End Sub

(hatena) 2021/10/10(日) 14:33


 1部だけ

 >   '最終列の取得
 >   Dim LastColumn As Long
 >   LastColumn = ws2.Cells(2, Columns.Count).End(xlLeft).Row

 取得しているのは行

 >LastColumn = ws2.Cells(2, Columns.Count).End(xlToLeft).columun
                                                         ^^^^^^^
(pen) 2021/10/10(日) 14:38

 ご提示のコードに対するアドバイスは他の方がして下さっているので、
 「正しい書き方」かどうか分かりませんが、ご参考まで。

 Sub test1()
 '重複の削除機能を使う場合
    Dim arr As Variant
    Dim i As Long, cnt As Long
    Dim buf As Long, rng As Range

    With Worksheets("元データ")
        With .Cells(2, 1).CurrentRegion
            arr = Intersect(.Cells, .Offset(1)).Value
        End With
        Set rng = .Range("A2:E2")
    End With
    With Worksheets("貼り付け先")
        buf = WorksheetFunction.Match(.Cells(2, 1).Value, rng, 0)
        cnt = 3
        For i = LBound(arr, 1) To UBound(arr, 1)
            If arr(i, 2) = .Cells(1, 1).Value Then
                cnt = cnt + 1
                .Cells(cnt, 1).Value = arr(i, 1)
                .Cells(cnt, 2).Value = arr(i, buf)
            End If
        Next i
        .Cells(4, 1).CurrentRegion.RemoveDuplicates Columns:=Array(1, 2)
    End With
 End Sub

 --

 Sub test2()
 'Dictionaryで重複を撥ねる場合
    Dim arr As Variant
    Dim i As Long
    Dim buf As Long, rng As Range

    Dim Dic As Object
    Set Dic = CreateObject("Scripting.Dictionary")

    With Worksheets("元データ")
        With .Cells(2, 1).CurrentRegion
            arr = Intersect(.Cells, .Offset(1)).Value
        End With
        Set rng = .Range("A2:E2")
    End With
    With Worksheets("貼り付け先")
        buf = WorksheetFunction.Match(.Cells(2, 1).Value, rng, 0)
        For i = LBound(arr, 1) To UBound(arr, 1)
            If arr(i, 2) = .Cells(1, 1).Value Then
                On Error Resume Next
                Dic.Add arr(i, 1), arr(i, buf)
                On Error GoTo 0
            End If
        Next i
        With .Cells(4, 1).Resize(Dic.Count)
            .Value = WorksheetFunction.Transpose(Dic.keys)
            .Offset(, 1).Value = WorksheetFunction.Transpose(Dic.Items)
        End With
    End With
 End Sub
(豆腐) 2021/10/10(日) 15:17

 最初のフィルタオプションの方は、
 訂正前
 .Range("XFD1").CurrentRegion.Clear
 .Rows(4).Delete

 訂正後
 .Range("XFD1:XFD2").Clear
 .Range("A4:B4").Delete shift:=xlUp

 こうしておけば、使えましたでしょうか。
 XFD1までデータが入っていたとしたら無理ですが・・・

 もしかしてA7以下にデータがあるということですかね。
 そうすると、元データのコードはあくまでも3種類限定ということですね。
 でしたら、
 Sub test1() '重複の削除機能を使う場合
 の方は無視して下さい。
 ※15:57 加筆修正
(豆腐) 2021/10/10(日) 15:45

hatena様
非常に丁寧なご説明、ありがとうございます!!
今、ひとつひとつ解読させて頂き、自分で書いたものと比べてみました。

> For〜Next で検索して、一致したらその時に出力して、ループを抜ける、

大変勉強になります。元データと貼り付け先の行数が違うのでどのように変数を使って処理すれば良いのか…頭の中が混乱してしまっているのがそのままコードに表れてしまっていました(*_*;
じっくり教えて頂いた事を整理して、完全に理解できるように頑張りたいと思います。

分かりやすい変数名を指定する事もおっしゃる通りですね。
今回のまさに大きな混乱の原因がここにありました。
本当にありがとうございました。

pen様
ありがとうございます!
まだ勉強したてでこのようなケアレスミスを連発させているので、エラーを減らせるようにしていきたいと思います!

豆腐様

 AdvancedFilterに加えて、Dictionaryまで教えて頂き大変ありがとうございます。
今回は、事例で端折ってしまっていますが、A7以下にも何百行とデータが続きます。
かつ、元データにはあって貼り付け先には存在しないコード、その逆…と存在する為、教えて頂いた方法で試してみたところ、そのようなケースの場合はうまく出来ませんでしたが、配列等もこれから出来るようになりたいと思っているので、要所要所に参考になる書き方があり、大変勉強になりました。
色々ご親切にありがとうございました。

(ココア) 2021/10/10(日) 17:44


今回、私が提示したコードは、質問のコードをもとに修正したもので、速度に関してはまったく考慮していないものなので、
元データ、貼り付け先のデータ数が多いとかなり遅くなる可能性があります。

元データの行数×貼り付け先の行数分のセル参照をすることになりますので。

高速化の方法としては、セル範囲を配列にして、それを対象にループする。
あるいは、Dictionaryを使って検索する。
その両方を併用する。
などの方法があります。

実データで実行してみて、あまりに遅いなら、
まずは、私のコードについて理解できてから、上記の高速化の手段を検討されるといいでしょう。
(hatena) 2021/10/10(日) 21:06


hatena様
ありがとうございます!
元データが5000行程度、貼り付け先が数百になると思います。遅くなりますかね、、
あの後ご教授頂きましたコードの意味は理解する所まで出来ましたが、
まだ数十件のデータでしか試せてないので、どの程度遅くなるのか実際のデータでやってみたいと思います。
配列はまだ勉強が追いついていないですが、とても便利と聞きますのでこれから勉強したいと思います。
このように行列マトリクスのような場合、調べるとdictionaryがよくヒットするので今回も最初やってみようとしましたが、
断念しました。。(T-T)
(ココア) 2021/10/10(日) 21:28

エクセルの標準機能が利用できます。
ややこしいロジックは考えなくて良いのがメリットです。

今回の例でば、
1)「フィルター詳細設定」で作業セルに抽出
2)「統合」で、貼付先に並べ替えて転記
という2ステップの操作で実現可能です。

これをマクロにすると

 Sub test()
    Dim 元データ As Range
    Dim 条件 As Range
    Dim 抽出先 As Range
    Dim 貼付先 As Range

    Set 元データ = Worksheets("元データ").Range("A2").CurrentRegion

    With Worksheets("貼り付け先")
        Set 条件 = .Range("E1:E2")
        条件.Value = Application.Transpose(Array("種目", .Range("A1").Value))
        Set 抽出先 = .Range("G1:H1")
        抽出先.Value = Array("コード", .Range("A2").Value)
        Set 貼付先 = .Range("A4")
    End With

    元データ.AdvancedFilter xlFilterCopy, 条件, 抽出先, True

    貼付先.CurrentRegion.Consolidate _
        抽出先.CurrentRegion.Address(, , xlR1C1, True), xlSum, False, True

       条件.Clear
       抽出先.CurrentRegion.Clear

 End Sub

(マナ) 2021/10/10(日) 22:58


 セル範囲を配列にするのとDictionaryを使って検索を併用する場合のサンプルコードを書いてみました。
これでかなり高速化するはずです。

 前回のコードを元に修正していますので、
前回のコードの意味を理解したうえで、
前回と今回を比べると、
どこをどのように配列やDictionaryに修正しているか分かると思います。

 Sub 行列取得_dic()
    Dim wsMoto As Worksheet
    Dim wsSaki As Worksheet
    Set wsMoto = Worksheets("元データ")
    Set wsSaki = Worksheets("貼り付け先")

    '元データと貼り付け先の表データを配列に格納。
    Dim aryMoto
    aryMoto = wsMoto.Cells(2, 1).CurrentRegion.Value
    Dim arySaki
    arySaki = wsSaki.Cells(4, 1).CurrentRegion.Resize(, 2).Value

    '検索月の列番号の取得
    Dim targetMonth As String
    targetMonth = wsSaki.Cells(2, 1)
    Dim i As Long
    Dim TargetCol As Long
    For i = 3 To UBound(aryMoto, 2)
        If aryMoto(1, i) = targetMonth Then
            TargetCol = i    '一致した列番号を取得
            Exit For
        End If
    Next i

    '検索種目
    Dim Syumoku As String
    Syumoku = wsSaki.Cells(1, 1)

    Dim dic As New Dictionary '要 Microsoft Scripting Runtime 参照設定
    For i = 2 To UBound(aryMoto)
        '種目が一致したら
        If aryMoto(i, 2) = Syumoku Then
            'コードをKeyに、検索月のデータをItemとして Dictionaryに追加
            dic(aryMoto(i, 1)) = aryMoto(i, TargetCol)
        End If
    Next

    Dim Code As String
    For i = 1 To UBound(arySaki)
        Code = arySaki(i, 1)
        'DictionaryからコードをKeyにして検索した月データを貼り付け先配列の2列目に出力
        arySaki(i, 2) = dic(Code)
    Next i
    '貼り付け先配列を貼り付け先セル範囲に出力
    wsSaki.Cells(4, 1).Resize(UBound(arySaki), UBound(arySaki, 2)).Value = arySaki
 End Sub
(hatena) 2021/10/10(日) 23:14

マナ様
ありがとうございます。
とてもシンプルですね。
ただ、私の最初の事例の書き方が良くなかったのですが、
実際には貼り付け先のA4セル以降にあらかじめコードが複数書いてあり、
そこには元データにも登場しないコードも存在します。
その為、フィルタ詳細設定ではその元データに存在しないコードが消えてしまいます。
ですが、この機能は色々な所で活用出来そうなので、今後の引き出しを増やすのに
大変勉強になりました。ありがとうございました!
(ココア) 2021/10/10(日) 23:16

hatena様
お忙しい中、大変ありがとうございます。
印刷して前回のコードとじっくり見比べながら検証してみたいと思います。
教えていただいた事を無駄にしないよう、配列とdictionaryを習得したいと思います。
どうしても、まだ勉強不足で、やりたい事を実現させる事がゴールになってしまっていますが、
今後理解を深めて、効率的、かつ速度も意識したプログラミングが出来るように頑張りたいと思います。
ほんと、ありがとうございました!
(ココア) 2021/10/10(日) 23:22

>実際には貼り付け先のA4セル以降にあらかじめコードが複数書いてあり、
>そこには元データにも登場しないコードも存在します。
>その為、フィルタ詳細設定ではその元データに存在しないコードが消えてしまいます。

消えません。そのために作業セルおよび「統合」機能を利用しています。

(マナ) 2021/10/10(日) 23:25


マナ様
大変失礼しました。
手元にパソコンがなく、自分の誤った解釈でお返事してしまいました。
まだ解読できておらず、どうして消えないのか分かっていませんが、
明日ステップインで一つずつ確認していきたいと思います。
大変、ありがとうございました!
(ココア) 2021/10/10(日) 23:40

マナ様
昨日はありがとうございました。
今、再現してやっと理解できました。
これは便利ですね。発想の転換で色々できますね。
AdvancedFilter、Consolidate、Address 等々、教えて頂いたコードをネットで解説見て、色々勉強になりました。
ご親切にありがとうございました。
(ココア) 2021/10/11(月) 08:45

hatena様
昨日はありがとうございました。
種目と年月をdicに格納しておいて、最後にコードと一致させるのですね…なるほど。。
配列もネットの説明だとよく分かりづらいのが、教えて頂いたものをステップインで確かめていったら
何とか理解することができました!!

最後の下記部分がまだ今いち理解出来ていないですが、今後理解を深めていきたいと思います。

    For i = 1 To UBound(arySaki)
        Code = arySaki(i, 1)
        'DictionaryからコードをKeyにして検索した月データを貼り付け先配列の2列目に出力
        arySaki(i, 2) = dic(Code)
    Next i
    '貼り付け先配列を貼り付け先セル範囲に出力
    wsSaki.Cells(4, 1).Resize(UBound(arySaki), UBound(arySaki, 2)).Value = arySaki

ほんとご丁寧なご対応に感謝いたします。ありがとうございました。
(ココア) 2021/10/11(月) 09:17


コメント返信:

[ 一覧(最新更新順) ]


YukiWiki 1.6.7 Copyright (C) 2000,2001 by Hiroshi Yuki. Modified by kazu.