I've modified the vba function below to suit my needs.
I have many workbooks with sheets that contain 4500+ rows, and I use the function to search for two given values (as boundaries). Then, it selects the rows as the range. Finally, do whatever on that range. The function:
Function GeoM(A, B)
Application.Volatile
Dim x As Integer
Dim y As Integer
Dim rng As Range
x = Application.WorksheetFunction.Match(A, Range("B:B"), 0) ' looking in col B
y = Application.WorksheetFunction.Match(B, Range("B:B"), 0) ' looking in col B
Set rng = Range(Cells(x, 18), Cells(y, 18)) 'Im working on col 18
GeoM = Application.WorksheetFunction.GeoMean(rng)
End Function
The problem is, this code works just fine except with GeoMeann. I noticed when the range of data is relatively small (number of data cells) it returns a value. However, if the range is larger than approx. 126 cells, it returns #value!.
I'm stuck and working on solving this issue. Is the GeoMean function limited to a given number of data?
Thanks
There appears to be a 170 character limit on my testing for earlier Excel versions (I tested in xl03), validated in this Mr Excel thread
(Xl10 worked fine on the longer dataset)
I also tried:
Using Evaluate
Using a 1D array
failed samples
Dim X
Set rng1 = Range("A1:A171")
MsgBox Evaluate("GeoMean(A1:A171)")
X = Application.Transpose(rng1)
MsgBox Application.WorksheetFunction.GeoMean(X)
to no avail.
So I think your two workarounds are either:
Inserting a formula via VBA into Excel and using this result
As per the MrExcel thread use the derivation of GeoMean, ie =EXP(AVERAGE(LN(Range)))
Suggested Approach
MsgBox Evaluate("EXP(AVERAGE(LN(A1:A171)))")
Thanks to brettdj, I fixed the function and it works now:
Function GeoM(A, B)
Application.Volatile
Dim x As Integer
Dim y As Integer
Dim rng As Range
Dim LnValue As Double
Dim count As Integer
x = Application.WorksheetFunction.Match(A, Range("B:B"), 0) 'look in col. B
y = Application.WorksheetFunction.Match(B, Range("B:B"), 0) 'look in col. B
Set rng = Range(Cells(x, 18), Cells(y, 18)) 'set range of rows on col# 18
Do
LnValue = LnValue + Math.Log(Cells(x, 18)) 'calculates sum of ln(value)
x = x + 1
count = count + 1 'calculates the total number of values
Loop Until x > y 'stop when x (upper row#) is greater than y (lower row#)
GeoM = Math.Exp((1 / count) * LnValue) 'GeoMean formula
End Function
This function searches a specified column for two values as upper and lower limits (Note: that means you shouldn't have repeated values in that column. In another words, the column should have unique values). Then, it finds the GeoMean of the values on other column, which has values fall in the same range of rows.
Related
I am having trouble getting Variants to subtract. I am pulling data from a spreadsheet and if one cell states a phrase then I need the code to subtract one cell from another. If the cell does not state a phrase then I need it to copy one cell to another. I can get the code to run but nothing happens.
Private Sub CommandButton1_Click()
Dim x As Variant, y As Variant, z As Variant, a As Integer, B As String
'getting values for data
x = Range("D2:D48").Value
y = Range("I2:I48").Value
z = Range("E2:E48").Value
B = "Total ISU Days: "
'The the cells are empty then subtract. This is not what I wanted to do but I can't think of extracting strings from variants.
If IsEmpty(Range("D2:D48").Value) = True Then
a = y - z
End If
Range("N2:N48").Value = a
Range("M2:M48").Value = B
End Sub
x = Range("D2:D48").Value
y = Range("I2:I48").Value
z = Range("E2:E48").Value
A Variant contains metadata about its subtype. In this case, x, y, and z are all arrays of variants.
a = y - z
The right-hand side of this expression simply cannot be evaluated, because {array1} - {array2} means nothing: operators (arithmetic or logical) work off values, not array of values.
What is a supposed to be? It's declared As Integer, so its value is capped at 32,767 (should probably be a Long). If you mean to add up all the values in y and subtract that total from the sum of all values in z, then you need to be more explicit about how you do that - you could use Application[.WorksheetFunction].Sum to add things up:
sumOfY = Application.Sum(Range("I2:I48"))
sumOfZ = Application.Sum(Range("E2:E48"))
a = sumOfY - sumOfZ
And then...
Range("N2:N48").Value = a
That will put the value of a in every single cell in the N2:N48 range - is that really what you mean to do?
Or maybe you meant to do this instead?
Range("N2:N48").Formula = "=IF(D2="""",I2-E2,0)"
That would make each cell in N2:N48 calculate the difference between I and E for each row where D is empty... and there's not really any need for any VBA code to do this.
Let's simplify a bit the task and say that the idea is to substract the values in Range("C1:C6") from the corresponding values in the left - Range("B1:B6"). Then write the corresponding results in column E:
Of course, this would be done only in case that all values in column A are empty. This is one way to do it:
Sub TestMe()
Dim checkNotEmpty As Boolean: checkNotEmpty = False
Dim substractFrom As Range: Set substractFrom = Worksheets(1).Range("B1:B6")
Dim substractTo As Range: Set substractTo = Worksheets(1).Range("C1:C6")
Dim MyCell As Range
Dim result() As Variant
ReDim result(substractFrom.Cells.Count - 1)
Dim areCellsEmpty As Boolean
For Each MyCell In substractFrom
If Len(MyCell) > 0 Then checkNotEmpty = True
Next
Dim i As Long
For i = LBound(result) + 1 To UBound(result) + 1
result(i - 1) = substractFrom.Cells(i) - substractTo.Cells(i)
Next
Worksheets(1).Range("E1").Resize(UBound(result) + 1) = Application.Transpose(result)
End Sub
The code could be improved further, saving all ranges to an Array, but it works quite ok so far.
The part with the +1 and -1 in the For-loop is needed as a workaround:
For i = LBound(result) + 1 To UBound(result) + 1
result(i - 1) = substractFrom.Cells(i) - substractTo.Cells(i)
Next
because the arrays start from index 0, but the Cells in a range start with row 1.
Worksheets(1).Range("E1").Resize(UBound(result) + 1) = Application.Transpose(result) is needed, to write the values of the result array to the column E, without defining the length of the range in E.
Ive been trying to make something in Excel to find multiple combinations of sums.
I have list of numbers that needs to be added together to be either within ranges of 500-510 or 450-460.
Only two numbers from the list can be used to find the sum. the numbers can not be used more than once. and giving the combinations of multiple results would be great. and if a number is not used it is ok.
I've tried the solver add-in and some other tips I found from this site but could not find something that gives multiple answers.
Does anyone know if this will be possible?
I'd break this into 2 tasks. First would be to simply generate all of the index pairs to test in the input array. That's relatively simple with recursive procedure. This one uses a private Type to store the pairs, but it could adapted to use some other method of storing the pairs:
Private Type Tuple
ValueOne As Long
ValueTwo As Long
End Type
Private Sub FindCombinations(elements As Long, ByRef results() As Tuple, _
Optional ByVal iteration As Long = 0)
If iteration = 0 Then ReDim results(0)
Dim idx As Long
For idx = iteration To elements - 1
Dim combo As Tuple
With combo
.ValueOne = iteration
.ValueTwo = idx
End With
results(UBound(results)) = combo
If iteration <> elements And idx <> elements Then
ReDim Preserve results(UBound(results) + 1)
End If
Next
If iteration < elements Then FindCombinations elements, results, iteration + 1
End Sub
Then, you use a "entry-point" procedure to generate the index combinations, use those to index into your source array, and apply your selection criteria:
Private Sub FindMatchingSets(testSet() As Long)
Dim indices() As Tuple
FindCombinations UBound(testSet) + 1, indices
Dim idx As Long, results() As Tuple
For idx = LBound(indices) To UBound(indices)
Dim tupleSum As Long
tupleSum = testSet(indices(idx).ValueOne) + testSet(indices(idx).ValueTwo)
If indices(idx).ValueOne <> indices(idx).ValueTwo And _
((tupleSum >= 500 And tupleSum <= 510) Or _
(tupleSum >= 450 And tupleSum <= 460)) Then
Debug.Print testSet(indices(idx).ValueOne) & " + " & _
testSet(indices(idx).ValueTwo) & " = " & tupleSum
End If
Next
End Sub
It isn't clear what you intend to do with the results, so this simply outputs the calculated values to the Immediate Window. Example calling code:
Private Sub Example()
Dim test(4) As Long
test(0) = 100
test(1) = 200
test(2) = 250
test(3) = 260
test(4) = 400
FindMatchingSets test
End Sub
May modify it according to your need & try
Sub test()
Dim X, Y, TRw, GotNum, First, Second As Long
TRw = 1
With ThisWorkbook.ActiveSheet
For X = 1 To 100 ' assumed col A1 to A100 is the list
GotNum = .Cells(X, 1).Value
If (GotNum >= 450 And GotNum <= 460) Or (GotNum >= 500 And GotNum <= 510) Then
.Cells(X, 1).Font.Color = RGB(255, 0, 0)
First = GotNum
For Y = X + 1 To 100
GotNum = .Cells(Y, 1).Value
If (GotNum >= 450 And GotNum <= 460) Or (GotNum >= 500 And GotNum <= 510) Then
Second = GotNum
TRw = TRw + 1
.Cells(TRw, 3).Value = First ' write 1st Number in Col C
.Cells(TRw, 4).Value = Second ' write 2nd Number in Col D
.Cells(TRw, 5).Value = First + Second ' write Sum of 1st & 2nd in Col C
End If
Next Y
End If
Next X
End With
End Sub
I think your question needs to be a little clearer in terms of what your expected output is (do you want a list of combos, or just to see the results?), but here's my solution.
I've put a list of 20 numbers in column Y, and assigned them all a letter (a through to t) in column X
Then I've built a matrix of the combinations of a to t, and have entered the following formula (the below is for cell C3, but it can be copied and pasted into all parts of the matrix)
=IF(C$2=$B3,"x",VLOOKUP(C$2,$X:$Y,2,FALSE)+VLOOKUP($B3,$X:$Y,2,FALSE))
I've then used conditional formatting to set the colour of the cells if they meet your criteria for the sum - you can do this by highlighting all the sums (cell C3:V22) and going to
home / conditional formatting / new rule...
picking the rule type format only cells that contain
and then in the drop down menus picking Cell Value / Between / Your high range
and then selecting a format (fill background colour, usually)
Do this once for the "high" sum, and once for the "low" sum. You can make the colours the same or different, depending on what you want to see.
I've also for reference included a reference to what the number is in Row 1 and column A. The formula for row 1 is (example is for C1, but it can be copied across)
=VLOOKUP(C2,$X:$Y,2,FALSE)
And the formula for column A is (example for A3) =VLOOKUP(B3,$X:$Y,2,FALSE)
The advantage of this approach is that it's all in excel (no code required), but the disadvantage is that it's hard to get a list of results. You could use a different formula to just return the sum (e.g. return the text "205+298") when it meets one of the conditions, but then it's still a pain to get it out of the matrix format and into a single list. Much easier using VBA
I'm creating an Excel spreadsheet and I want to create a formula based on the contents of a cell. A basic example is below:
So, cells 1, 2 and 3 have values YxZ. I want to use the Y value and the Z value and do calculations based on each. I don't want to have to have separate cells for Y and Z.
So I'd want to single out the Y and do a simple addition to get 11. I'd then want to get the average of Z, which would be 225 in this case.
Can this be done? I'm by no means an Excel wizard!
Here is a variation of #ScottCraner 's excellent answer:
Function Apply(Formulas As Range, operator As String, operand As Long, func As String) As Variant
Dim terms As Variant
Dim cell As Range
Dim expression As String
Dim i As Long
ReDim terms(1 To Formulas.Cells.Count)
i = 1
For Each cell In Formulas.Cells
terms(i) = Split(cell, operator)(operand - 1)
i = i + 1
Next cell
expression = func & "(" & Join(terms, ",") & ")"
Apply = Application.Evaluate(expression)
End Function
You pass a range of expressions of the form operand operator operand, a string representing the operator, and the integer 1 or 2 denoting either the operand before or the operand after the operator, as well as a string representing the function that you want to apply to the corresponding operands.
It works like this:
In the first column I use operand = 1 and in the second column I use operand = 2.
you need array formulas to extract the text around x in each cell, convert to numbers and then sum the values together => confirm with Ctrl+Shift+Enter after copy&pasting to cells I2 (sum of Y) and J2 (sum-product of Z divided by sum of Y):
=SUM(IFERROR(VALUE(LEFT($A2:$H2,FIND("x",$A2:$H2)-1)),0))
=SUM(IFERROR(VALUE(LEFT($A2:$H2,FIND("x",$A2:$H2)-1)) * VALUE(MID($A2:$H2,FIND("x",$A2:$H2)+1,255)),0)) / $I2
A vba Option.
This User Defined Function will return the two numbers:
Function sumaver(rng As Range, dm As String)
Dim sm As Double
Dim avg As Double
Dim cnt As Long
Dim r As Range
Dim str() As String
Dim tmp(1 To 2)
For Each r In rng
str = Split(r.Value, dm)
If UBound(str) = 1 Then
sm = sm + str(0)
avg = avg + str(1)
cnt = cnt + 1
End If
Next r
tmp(1) = sm
tmp(2) = avg / cnt
sumaver = tmp
End Function
Put this in a module attached to the desired workbook. DO NOT put this in the worksheet or ThisWorkbook code.
Being that it returns an array of two numbers it can be called in two ways.
The first is to highlight two consecutive horizontal cell and in the first enter:
=sumaver(A2:H2,"x")
Then hit Ctrl-Shift-Enter instead of Enter. If done correctly Excel will automatically put {} around the formula and fill the second cell. The first will be the sum the second the average.
But if you wan the individual parts in different non contiguous horizontal cells then you can call the sum with this formula:
=INDEX(sumaver(A2:H2,"x"),1,1)
and the average with:
=INDEX(sumaver(A2:H2,"x"),1,2)
The parameters of the sumaver() function are sumaver(Range, character on which to split)
You can include empty cells in the range, they will be ignored.
I have a macro which is written to perform an OLS regression on data that is selected by the user. This is part of a larger add in that I am writing but I am stuck on what I think must be somewhat of a simple issue. I keep getting a subscript out of range error and I think its because I am getting a different sized matrix to what I am expecting.
The sub takes two variables as its arguments and calculated the OLS estimator given the specification. The y variable is always a n x 1 range (one column and multiple row) and the X variable is a n x m range (can be multiple columns and rows). When this function is used when X is a single column range, the For... Next block works for the following code:
For bcnt = 1 To k
Cells(bcnt, 1).Value = b(bcnt)
Next bcnt
But if the X variable is a multiple column range this won't work and it has to be the following:
For bcnt = 1 To k
Cells(bcnt, 1).Value = b(bcnt,1)
Next bcnt
I can't understand why as by my understanding b should always be a one dimensional array.
Would appreciate any help.
The actual sub:
Sub OLSregress(y As Variant, X As Variant)
Dim Xtrans, XtransX, XtransXinv, Xtransy As Variant
Dim outputsheet As Worksheet
Dim b As Variant
' The equation for this estimator is b=[X'X]^(-1)X'Y
Xtrans = Application.WorksheetFunction.Transpose(X)
XtransX = Application.WorksheetFunction.MMult(Xtrans, X)
XtransXinv = Application.WorksheetFunction.MInverse(XtransX)
Xtransy = Application.WorksheetFunction.MMult(Xtrans, y)
b = Application.WorksheetFunction.MMult(XtransXinv, Xtransy)
k = Application.WorksheetFunction.Count(b)
Set ouputsheet = Sheets.Add(, ActiveSheet)
ActiveSheet.Name = "Regression Output"
For bcnt = 1 To k
Cells(bcnt, 1).Value = b(bcnt, 1)
Next bcnt
End Sub
When you are referring to a range or are bringing in data from a sheet the array is always a 2 dimensional array. The first dimension is rows and the second is the columns.
This is a common point of confusion in VBA for excel because it's done without your intervention.
Your code is correct.
For more in-depth information check out this post
I want to calculate a moving average of the last, say 20, numbers of a column. A problem is that some of the cells of the column may be empty, they should be ignored. Example:
A
175
154
188
145
155
167
201
A moving average of the last three would be (155+167+201)/3. I've tried to implement this using average, offset, index, but I simply don't know how. I'm a little bit familiar with macros, so such a solution would work fine: =MovingAverage(A1;3)
Thanks for any tips or solutions!
{=SUM(($A$1:A9)*(ROW($A$1:A9)>LARGE((ROW($A$1:A9))*(NOT(ISBLANK($A$1:A9))),3+1)))/3}
Enter this with control+shift+enter to make it an array formula. This will find the latest three values. If you want more or less, change the two instances of '3' in the formula to whatever you want.
LARGE((ROW($A$1:A9))*(NOT(ISBLANK($A$1:A9))),3+1)
This part returns the 4th highest row number of all the cells that have a value, or 5 in your example because rows 6, 8, and 9 are the 1st through 3rd highest rows with a value.
(ROW($A$1:A9)>LARGE((ROW($A$1:A9))*(NOT(ISBLANK($A$1:A9))),3+1))
This part returns 9 TRUEs or FALSEs based on whether the row number is larger than the 4th largest.
($A$1:A9)*(ROW($A$1:A9)>LARGE((ROW($A$1:A9))*(NOT(ISBLANK($A$1:A9))),3+1))
This multiplies the values in A1:A9 by those 9 TRUEs or FALSEs. TRUEs are converted to 1 and FALSEs to zero. This leaves a SUM function like this
=SUM({0;0;0;0;0;155;0;167;201})/3
Because all the values above 155 don't satisfy the row number criterion, the get multiplied by zero.
If you are going to use a UDF it will only recalculate correctly when you change the data if the parameters include all the range of data you want to handle.
Here is a moving average UDF that handles entire columns and contains some error handling. You can call it using by entering the formula =MovingAverage(A:A,3) into a cell.
Function MovingAverage(theRange As Range, LastN As Long) As Variant
Dim vArr As Variant
Dim j As Long
Dim nFound As Long
Dim dSum As Double
On Error GoTo Fail
MovingAverage = CVErr(xlErrNA)
'
' handle entire column reference
'
vArr = Intersect(Application.Caller.Parent.UsedRange, theRange).Value2
If IsArray(vArr) And LastN > 0 Then
For j = UBound(vArr) To 1 Step -1
' skip empty/uncalculated
If Not IsEmpty(vArr(j, 1)) Then
' look for valid numbers
If IsNumeric(vArr(j, 1)) Then
If Len(Trim(CStr(vArr(j, 1)))) > 0 Then
nFound = nFound + 1
If nFound <= LastN Then
dSum = dSum + CDbl(vArr(j, 1))
Else
Exit For
End If
End If
End If
End If
Next j
If nFound >= LastN Then MovingAverage = dSum / LastN
End If
Exit Function
Fail:
MovingAverage = CVErr(xlErrNA)
End Function
Just a quick solution:
Supposing your numbers are on the cells A2:A10, put in B10 the following formula:
=IF(COUNT(A8:A10)=3,AVERAGE(A8:A10),IF(COUNT(A7:A10)=3,AVERAGE(A7:A10),"too many blanks"))
Dragging up the formula you get the moving average
If there is the possibility of two consecutive blank you could nest another if, more than that and this solution became too complicated
I have written a short script in VBA. Hopefull it does what you want. Here you are:
Function MovingAverage(ByVal r As String, ByVal i As Integer) As Double
Dim rng As Range, counter As Long, j As Integer, tmp As Double
Set rng = Range(r)
counter = 360
j = 0
tmp = 0
While j < i + 1 And counter > 0
If Len(rng.Offset(j, 0)) > 0 Then
tmp = tmp + rng.Offset(j, 0).Value
End If
j = j + 1
counter = counter - 1
Wend
MovingAverage = CDbl(tmp / i)
End Function
1) I have set limit to 360 cells. It means that the script will not look for more than 360 cells. If you want to change it then change the initial value of counter.
2) The script returns not rounded average. Change the last row to
MovingAverage = Round(CDbl(tmp / i),2)
3) The use is just like you wanted, so just type =MovingAverage("a1";3) into the cell.
Any comments are welcome.