I am executing the VBA code below in the following file:
http://www.filedropper.com/error13
I get an Error 13 Type Mismatch.
Here is the macro
Aggregate, Collate and Transpose rows into columns
Works fine when I select some of the rows (for example id 1001 or 1003 and 1004 together but when I try to process more rows I get Error 13.
I'm trying to process each id at a time but I have about 100..
For the sake of curiosity and intellectual exercise, I took a stab at the array processing method originally submitted by ZygD in the original question with a mind to overcome the Runtime error '13': Type mismatch error on larger data sets.
The ReDim Preserve statement can only redimension the last rank while preserving existing values already stored subject to the fact that you are raising the size of the array dimension and not shrinking it. This is what msdn.microsoft.com has to say on the subject:
Resizing with Preserve. If you use Preserve, you can resize only the last dimension of the array. For every other dimension, you must specify the bound of the existing array.
For example, if your array has only one dimension, you can resize that dimension and still preserve all the contents of the array, because you are changing the last and only dimension. However, if your array has two or more dimensions, you can change the size of only the last dimension if you use Preserve.
Due to the orientation of the data peeled out of the worksheet, the first rank was the dimension to grow with ReDim so the orientation was flipped with Application.Transpose, ReDim'ed with Preserve then flipped back. As the array grew with additional records, the Application.Transpose quickly reached its maximum capacity to reorient the array. I found some older documentation on this in XL: Limitations of Passing Arrays to Excel Using Automation but it is horribly out of date.
My solution was to transpose the values from ar1 into ar2 on the fly so that ar2 could be redimensioned without reorientation. Once processing was complete, the results were in the wrong orientation. To get the values back into the worksheet in the correct orientation, I wrote a helper function that transposed ar2 back into a truncated ar1. This pseudo-transpose was only needed once; just before stuffing the new aggregated values back into the reporting area.
Modified sub code:
Sub jpd_Transposing()
Const sDestination As String = "D2"
Dim ar1 As Variant
Dim ar2 As Variant
Dim i As Long 'counter
With ActiveSheet
ar1 = .Range("A2:B" & .Cells(Rows.Count, 1).End(xlUp).Row).Value
ReDim ar2(1 To 2, 1 To 1)
ar2(1, 1) = ar1(1, 1): ar2(2, 1) = ar1(1, 2)
For i = 2 To UBound(ar1, 1)
If ar1(i, 1) = ar2(1, UBound(ar2, 2)) Then
ar2(2, UBound(ar2, 2)) = ar2(2, UBound(ar2, 2)) & ar1(i, 2)
ElseIf ar1(i, 1) = vbNullString Then
ar2(2, UBound(ar2, 2)) = ar2(2, UBound(ar2, 2)) & " "
Else
ReDim Preserve ar2(1 To 2, 1 To UBound(ar2, 2) + 1)
ar2(1, UBound(ar2, 2)) = ar1(i, 1)
ar2(2, UBound(ar2, 2)) = ar1(i, 2)
End If
Next
ar1 = my_2D_Transpose(ar1, ar2)
.Range(sDestination).Resize(UBound(ar1, 1), UBound(ar1, 2)) = ar1
End With
End Sub
Function my_2D_Transpose(a1 As Variant, a2 As Variant)
Dim a As Long, b As Long
ReDim a1(1 To UBound(a2, 2), 1 To UBound(a2, 1))
For a = LBound(a2, 1) To UBound(a2, 1)
For b = LBound(a2, 2) To UBound(a2, 2)
a1(b, a) = Trim(a2(a, b))
Next b
Next a
my_2D_Transpose = a1
End Function
So now you might be wondering just how much improvement over the original worksheet based routine there was with the arrayed memory processing. As that was the logical next step, I ran both with a timer noting start and stop.
Sub timed()
Application.ScreenUpdating = False
Application.EnableEvents = False
Debug.Print Timer
Call concatenate_and_transpose_to_delim_string
Debug.Print Timer
Call jpd_Transposing
Debug.Print Timer
Application.EnableEvents = True
Application.ScreenUpdating = True
End Sub
Both of the report data results were identical. Note that I turned off screen updating and event handling for the duration of the test. This likely improved the worksheet method more than it improved the array method but I thought it was fair given that these are pretty standard techniques for improving the efficiency of a macro.
Timed Results:
Test environment: 45,894 rows × 2 columns of raw data converted to 123 rows × 2 columns of aggregated report data using a business class i5/8Gb based laptop (Win7, Excel 2010 version 14.0.7145.5000 (32-bit)
concatenate_and_transpose_to_delim_string (worksheet) .... 00:01.01 seconds¹
jpd_Transposing (memory array) ............................................... 00:00.07 seconds¹
¹Test was run several times. Times are typical.
Conclusions:
Okay, so we picked up almost a full second using a variant memory array over the worksheet read/write but that is still a whopping 93% improvement in efficiency of processing identical data to identical results. I've converted other long-running routines from worksheet driven to arrayed memory; the results were at least as appreciable and those were devoted to more repetitious lookup-type operations in large data matrices.
Was it worth it? That's pretty much up to the individual user and situation. Certainly there are benefits to be had but I write worksheet based code a lot faster than array based code so unless this ran several times a day every day, I probably wouldn't bother. The size of the project would also be a factor as benefits would increase with the amount of work to be done. Still, it's good to keep the methods used with memory array methods fresh in the mind and a mashup of methods may produce the best result in some cases.
FWIW, the VBA Trim function used in the transpose helper function produced no measurable detrimental effect (e.g. additional time) whether it was used or not and seemed the best place to ensure that the end result did not have a trailing space character left over from string concatenation.
Related
I have implemented this method to multiply every array element by a number held in a variable. It is terribly slow.
Is there an accepted "fastest" way to multiply every element in a range by a constant? Or at least one which is not as slow? I have to do this 10 times and it takes a couple of minutes.
MultFactor = 10
For Each cell In Sheet1.Range("B3:B902")
cell.Value = cell.Value * MultFactor
Next cell
The solution cited in Multiply Entire Range By Value? multiplies by a constant (not a variable). If I use this code (changing the range from "A1:B10" to "B3:B902"), I get a nonsense answer.
Dim rngData As Range
Set rngData = Sheet12.Range("B3:B902")
rngData = Evaluate(rngData.Address & "*2")
My original values in B3:B902 are zero for the first 100 elements or so and then increase a bit and finally decrease and have another run of zeros, but what ends up in my range is a series of numbers that clobbers everything in my range. It begins at -224.5 and decreases by 0.5 all the way to the last cell.
-224.5
-224.0
-223.5
etc.
Even if that worked, how would I modify it to use the variable MultFactor?
This will be hundreds to thousands of times faster. The difference is that all of the calcs are done to a VBA array instead of directly to worksheet cells, one by one. Once the array is updated it is written back to the worksheet in one go. This reduces worksheet interaction to just two instances, reading the array and writing it. Reducing the number of instances that your VBA code touches anything on the worksheet side is critical to execution speed.
Sub Mozdzen()
Const FACTOR = 10
Const SOURCE = "B3:B902"
Dim i&, v
v = Sheet1.Range(SOURCE)
For i = 1 To UBound(v)
v(i, 1) = v(i, 1) * FACTOR
Next
Sheet1.Range(SOURCE) = v
End Sub
Building on the above idea, a better way to manage the code is to encapsulate the array multiplication with a dedicated function:
Sub Mozdzen()
Const FACTOR = 10
Const SOURCE = "B3:B902"
With Sheet2.Range(SOURCE)
.Value2 = ArrayMultiply(.Value2, FACTOR)
End With
End Sub
Function ArrayMultiply(a, multfactor#)
Dim i&
For i = 1 To UBound(a)
a(i, 1) = a(i, 1) * multfactor
Next
ArrayMultiply = a
End Function
You need:
rngData = Sheet12.Evaluate(rngData.Address & "*2")
since the address property doesn't include the sheet name by default (so your formula is evaluated in the context of the active sheet's range B3:B902)
Then it would need:
rngData = Sheet12.Evaluate(rngData.Address & "*" & MultFactor)
to add in your variable.
I saw quite a number of questions/answers along these lines, but after reading a bunch of them, I'm still confused. I'm sorry if this is the nᵗʰ time a variant of this has been asked.
I can't figure out why this code dies on line 5 with a "subscript out of range" error in VBA (Excel for Mac v16.38):
Public Function array_test()
Dim arr As Variant
Dim array_slice As Variant
arr = Range("TestData!B27:F32").Value2
array_slice = arr(2) 'dies here with error 9
array_test = array_slice
End Function
Looking at the values pane, arr is clearly a Variant/Variant(1 to 6, 1 to 5) with all the expected data. There is nothing special about the cells, just non-formula data.
Even if I change the declarations to arr() and array_slice() or remove ".Value2", I get the same results. Even trying Application.WorksheetFunction.Index(arr, 2) rather than arr(2) gets me nowhere.
What am I missing?
P.S. I'm a C/Python programmer normally, so I'm thinking of arrays in those terms.
Usually one would loop an 2D-array for it's elements, however, since you specifically mentioned you would like to slice it through VBA, you could use Application.Index. For example try:
array_slice = Application.Index(arr, 2, 0) 'Slice 2nd row into 1D-array.
The idea here is to feed Application.Index with a static '2' which represents the row of interest. In the same fashion you could slice a specific column of interest, though if you need this to be an 1D-array, you'd need to use Application.Transpose, however there are limitations to this method:
With Application
array_slice = .Transpose(.Index(arr, 0, 2)) 'Slice 2nd column into 1D-array.
End With
When you copy data from a Range and the Range contains more than one cell, you get n 2-dimensional array in any case (even if you have only one row or one column).
To access a single value from that array, you have to provide both indices, like arr(2, 1). However, if you want to get a one-dimensional array, containing all values from a row (first index) or a column (second index), you need to create that array by your own - there is no slice function in VBA. You can dimension an array at runtime using the ReDim-command:
Dim array_slice(), i As Long
ReDim array_slice(LBound(arr, 1) To UBound(arr, 1))
For i = LBound(arr, 1) To UBound(arr, 1)
array_slice(i) = arr(i, 2)
Next i
To get the values of a column, use
Dim array_slice(), i As Long
ReDim array_slice(LBound(arr, 2) To UBound(arr, 2))
For i = LBound(arr, 2) To UBound(arr, 2)
array_slice(i) = arr(2, i)
Next i
I'm trying to create a function to calculate the required rate of return of a retirement fund. The situation is this: The retiree has a target amount of funds for their retirement account at the commencement of their retirement.
They also have a target withdraw amount that they would like to draw down every year, for a target amount of years.
For example: A retirement fund amount of $1,000,000. A annual withdraw amount of $100,000, and a target of 15 years.
The code I have written is as follows:
Function RequiredReturn(retire_amnt, annual_spending, n_year)
Dim arr()
ReDim arr(0)
arr(0) = -retire_amnt
ReDim arr(1 To n_year)
For i = 1 To n_year
arr(i) = annual_spending
Next i
RequireReturn = Application.WorksheetFunction.IRR(arr)
End Function
The goal of this function is to:
Create an array with the first value (0) = the negative retirement amount
Fill the array from (1 to n) with the annual_spending amount
Call the IRR function on this to find the minimum required annual return to be able to achieve this cash flow.
If you were to do by entering this array into cells, and then use the IRR function, the required return would be 5.56%
Obviously this is not working.
How can I change this to populate the array correctly?
If populated correctly, is the IRR function being applied correctly?
Or more directly than your initial approach, use the RATE formula
This calls the initial amount as a negative, ie
Sub Test()
MsgBox Format(RequiredReturn(-1000000, 100000, 15), "#.0000%")
End Sub
options
RequiredReturn = Application.Evaluate("=RATE(" & n_year & "," & annual_spending & "," & retire_amnt & ")")
RequiredReturn = Application.Rate(n_year, annual_spending, retire_amnt)
Your assign the array twice and the second assignment overrides the first - that is the error in your array logic.
You can fix the array by defining it as:
ReDim arr(0 To n_year)
Which will give it n_year + 1 slots, where 0th slot can take the lump sum (retire_amnt) and the slots from 1-n_year take the annuity payments (annual_spending). Then you have the correct data structure for an IRR calculation.
The code below works and gives you the expected result of 5.56%
Option Explicit
Sub Test()
MsgBox Format(RequiredReturn(1000000, 100000, 15), "#.0000%")
End Sub
Function RequiredReturn(retire_amnt, annual_spending, n_year)
Dim arr() As Variant
Dim i As Long
ReDim arr(0 To n_year)
arr(0) = -retire_amnt
For i = 1 To n_year
arr(i) = annual_spending
Next i
RequiredReturn = Application.WorksheetFunction.IRR(arr)
End Function
I've mainly used IRR in the case of a capital out-flow followed by a series of equally timed cash in-flows. But in the case of annuity rate of return my understanding is that IRR can be used as well because it doesn't matter if the starting amount is a credit or debit as long as the subsequent cash in/out-flows are of the opposite signage. You probably don't want to risk your pension on that comment though :)
I have a spreadsheet, BO2009, that is 300k rows long. Only one column contains a formula The others are all pasted values so only one formula needs to be calculated in the entire workbook. Here is the formula: =IFERROR(INDEX('RE2009'!H:H,MATCH('BO2009'!A2,'RE2009'!A:A,0)),1) This formula is copied down to the bottom of the sheet, so 300k times.
RE2009 sheet has 180k rows. 'RE2009'!H:H contains decimal numbers and 'RE2009'!A:A, 'BO2009'!A:A contain ID codes--an 8 character combination of numbers and letters. Both 'RE2009'!A:A, 'BO2009'!A:A are formatted as general.
I use INDEX/MATCH all the time and while most of my spreadsheets are not 300k long, 60k-100k is typical. Right now it takes a couple minutes of my CPU devoting 99% to Excel in order to finish the calculation.
Is that normal? Is there any way to improve Excel's performance?
On top of that I am getting inaccurate results: instead of 0.3 the lookup produces an error.
As suggested, I have filtered the BO2009 sheet down to 80k rows, but still have the same issues. I decided to look at a single formula in particular: =IFERROR(INDEX('RE2009'!H:H,MATCH('BO2009'!A108661,'RE2009'!A:A,0)),1) to see if it worked correctly. The ID that it is looking for with the MATCH function is the 3rd entry in the lookup array, but it still isn't able to produce the correct value (0.3)
It seems that you've found a satisfactory solution to your problem(s) but as a matter of curiosity, you may wish to time this against your current formula based solution to see if there is a measurable increase in speed.
Sub index_match_mem()
Dim v As Long, vVALs As Variant, vTMP As Variant
Dim dRE2009 As Object
Debug.Print Timer
Application.ScreenUpdating = False
With Worksheets("RE2009")
With .Cells(1, 1).CurrentRegion
With .Resize(.Rows.Count, 8)
vTMP = .Cells.Value2
End With
End With
End With
Set dRE2009 = CreateObject("Scripting.Dictionary")
dRE2009.CompareMode = vbTextCompare
For v = LBound(vTMP, 1) To UBound(vTMP, 1)
If Not dRE2009.exists(vTMP(v, 1)) Then _
dRE2009.Add Key:=vTMP(v, 1), Item:=vTMP(v, 8)
Next v
With Worksheets("BO2009")
With .Cells(1, 1).CurrentRegion
With .Resize(.Rows.Count - 1, 2).Offset(1, 0)
vVALs = .Cells.Value2
For v = UBound(vVALs, 1) To LBound(vVALs, 1) Step -1
If dRE2009.exists(vVALs(v, 1)) Then
vVALs(v, 2) = dRE2009.Item(vVALs(v, 1))
Else
vVALs(v, 2) = 1
End If
Next v
.Cells = vVALs
End With
End With
End With
dRE2009.RemoveAll: Set dRE2009 = Nothing
Application.ScreenUpdating = True
Debug.Print Timer
End Sub
This will produce static values in column B of the BO2009 worksheet. The elapsed start and stop in seconds will be in the VBE's Immediate window (Ctrl+G)
First off, I apologize for starting a new thread but the original got confusing because I couldn't articulate my ask well (Link to original thread: Dynamic Nested Loops for Autofilter in Excel VBA). But now I have actually written the program to the way I like except using a switch statement instead of the more dynamic use of nested looping.
edit:
RSum is used to store a range and a boolean. The user selects the header cell for a column and chooses whether they want get a summation of that column or a unique count when summarizing. This allows for a collection of these objects to allow summarizing of multiple columns. This input wasn't so bad to make dynamic. The next input which starts as rtemp and ends as array1, is again the user selects the header cell for a column but this it takes the values in that column and saves a unique list to array1. With this list a for loop loops through the array using its value as criteria for an autofilter. For each step in the loop after the autofilter, the summary is calculated using the SumThisA taking the RSum object collection as an input. The data is laid out in columns where each row is a unique record.
So the question is, for the below code, I want the user to be able to select the number of categories to summarize by, have a popup to fill in those ranges (can figure this out), and then run a filter as such:
for i = 0 to UBound(array1)
Autofilter criteria1:=array1(i)
for j = 0 to UBound(array2)
Autofilter criteria1:=array2(j)
......
for x = 0 to UBound(arrayx)
Autofilter criteria1:=arrayx(x)
aSum(i,j,....x) = somefunction
Now I understand I would need to use a recursive function, but having never used one before and the somewhat complexity of this program, it is out of my understanding. Would anyone be able to help explain how to use it in this context? Plus because of the generalization of this program, it could be a useful tool for many people.
'---------Initialize Arrays---------------'
t = sMax - 1
Dim aSum()
ReDim aSum(UBound(arr1), t)
'---------------------Perform Summary----------------'
For i = LBound(arr1) To UBound(arr1)
If i = 0 Then
Data.AutoFilter field:=afield, Criteria1:=arr1, Operator:=xlFilterValues
Else
Data.AutoFilter field:=afield, Criteria1:=arr1(i)
End If
temp = SumThisA(SumValues, sMax)
For j = LBound(temp) To UBound(temp)
aSum(i, j) = temp(j)
Next j
Next i
Sum of Dollars For:
1. arrayA(1)-------100
- arrayB(1)------30
- arrayB(2)------70
2. arrayA(2)-------200
- arrayB(1)-----120
- arrayB(2)------80
3. Total-----------300
Here's a very kludgy example of recursion for what it seems you want to do. I faked up some criteria, so don't get hung up on how I'm testing for that, what's important is how the function Filter functions recursively. If I could pinpoint more exactly what you wanted I could craft it more precisely, and with less hardcoding.
Test Harness:
Public Sub Test()
Dim FilteredArray As Variant, cArray As Variant, working Array As Variant
Dim criteria As Integer
criteria = 1
ReDim criteriaArray(1 To 2)
cArray(1) = Range("C1").Value
cArray(2) = Range("C2").Value
Set workingArray = Range("A1:A7")
FilteredArray = Filter(workingArray, 7, cArray, criteria)
Range("D1") = FilteredArray
End Sub
Recursive Filter Function:
Public Function Filter(workingArray As Variant, index As Integer, _
criteriaArray As Variant, criteria) As Variant
Dim tempArray As Variant, i As Integer
ReDim tempArray(1 To 1)
For i = 1 To index
If Mid(workingArray(i), criteria, 1) = criteriaArray(criteria) Then
ReDim Preserve tempArray(1 To UBound(tempArray) + 1)
tempArray(UBound(tempArray) - 1) = workingArray(i)
End If
Next i
ReDim Preserve tempArray(1 To UBound(tempArray) - 1)
If criteria < 2 Then
Filter = Filter(tempArray, UBound(tempArray), criteriaArray, criteria + 1)
Else
Filter = tempArray
End If
End Function
Have you considered using a pivot table ? Your requirements seem very close to that functionality...