用户定义的函数,它接受 Excel VBA 中的连续和不连续范围

User defined function which accepts both continuous and discontinuous ranges in Excel VBA

基础问题

我正在尝试在 Excel 中创建我自己的 VBA 函数,我希望它能够基本上接受任何类型的输入并将输入视为向量,但我还没有还没有弄清楚如何对连续(例如 (A1:A10) 或 (A1:R1))和不连续(例如 (A1;B5;G12))范围执行此操作。我可以让这些功能适用于任何一种,但不能同时适用于两种类型。

我希望这样做的原因是我希望制作我自己的 AVERAGE 和 STDEV.S 版本,它可以处理单元格中的 #N/A 值。我知道我可以使用 AVERAGEIF(range;"<>#N/A") 来计算平均值,但 AVERAGEIF 不允许我使用不连续的范围,据我所知,没有STDEV.S 这样的替代方案。

我的数据背景

我的数据是从我用各种化学方法测量的几个样本中获得的。我每天准备一个样品,然后用一天剩下的时间测量上面的东西。每个样本都被视为 "one experiment",每个实验都存储为单独的工作表,我在其中存储来自所有不同分析方法的数据,并进行任何数据处理以使数据具有可比性(例如,根据摩尔浓度计算摩尔浓度,对温度差异进行调整, ETC);我还存储了很多半不相关的信息(例如最终结果不需要但仍然需要保留的注释)。长话短说,将所有运行都存储在一个工作表中的数据太多了,因为这会使它看起来太乱了,而且处理单个实验也太乱了,尤其是当我向数据堆中添加一个新实验时;我目前的方法允许我简单地复制现有的工作表并将新数据粘贴到旧方程式中。然后将处理后的数据链接到一个 "overview" 工作表,我在其中列出了最有趣的数据,其结构方式使我可以轻松比较不同测量的值。链接是通过 INDIRECT 完成的,这样我就可以轻松地添加来自新实验的新信息。由于数据来自实验,因此必然会有数据丢失,我使用 #N/A 来弥补这样的漏洞,如果数据丢失,从一个工作表链接到另一个工作表会产生“0”。我知道我可以用一个简单的破折号 (-) 或类似的东西替换 #N/A,这将使内置的 AVERAGE 和 STDEV.S 工作,但我想使用相同的数据数组进行绘图看起来好像只有 #N/A 会从图中删除数据点,因为 excel 中的图形将破折号视为零值。

我的 "overview" 工作表上的数据排列为

    Date    pH    Na+ conc    K+ conc  ...lots of other variables
    Date 1  7.4     140        3       ... 
    Date 2  7.1     #N/A       4       ...
    ....    ...     ...        ...     ...
    Date N  7.3     143        3.5     ...

适用于连续范围的代码

到目前为止,支持连续范围的以下代码示例计算了包含#N/A 值的单元格的标准差。当我 select 整列(或列的连续部分)时,此代码完美运行,但如果我 select 单元格的不连续范围。

Function StdevNaN_S(xRange)

'Sample Standard deviation which excludes NaN values

xR = xRange 'I can, for some strange reason, not use UBound unless I re-store the data in xR...
NoE1 = UBound(xR, 1) 'Number of Elements along dimension 1 (columns)
NoE2 = UBound(xR, 2) 'Number of Elements along dimension 2 (rows)
NoE = NoE1 * NoE2 'Total Number of Elements (this way makes it work regardless of row or column range)

'Need to first calculate the NaN excluded average value - could use the AVERAGEIF to simplify, but that will break if the range is discontinuous
xSum = 0
xAmount = 0
For N = 1 To NoE
    If IsNumeric(xRange(N)) Then
        xSum = xSum + xRange(N)
        xAmount = xAmount + 1 'counting how many cells that are used in the sum, used as the divisor in the average and the variance expression. Couldn't use the "CountIf" expression as it counted cells which contained text
    Else
    End If
Next N
xAvg = xSum / xAmount

'Uses the average in the variance calculation
xSum = 0
For N = 1 To NoE
    If IsNumeric(xRange(N)) Then
        xSum = xSum + (xRange(N) - xAvg) ^ 2 'Summing up (x - x_avg) ^ 2, which is the dividend of the variance expression
    Else
    End If
Next N

StdevNaN_S = (xSum / (xAmount - 1)) ^ 0.5 'the sample standard deviation is the square root of the corrected variance

End Function

我的问题是我想对部分数据进行平均和标准差计算。例如,在日期 1、5、19 和 34 生产的样品是用特定的化学品库存生产的,而日期 2:4、6:11 和 25:33 来自第二个库存和从第三个休息,所以我想知道是否有具体股票的影响。

适用于不连续范围的代码

我在 cpaerson.com 上找到了一个示例,它展示了如何允许一个函数采用不连续的范围并将其视为一个向量。他们的例子是

Function SumOf(ParamArray Nums() As Variant) As Variant
''''''''''''''''''''''''''''''''''
' Add up the numbers in Nums
''''''''''''''''''''''''''''''''''
    Dim N As Long
    Dim D As Double
    For N = LBound(Nums) To UBound(Nums)
        If IsNumeric(Nums(N)) = True Then
            D = D + Nums(N)
        Else
            SumOf = CVErr(xlErrNum)
            Exit Function
        End If
    Next N
    SumOf = D
End Function

但是,此函数 用于不连续的 select 离子 - 如果我 select 例如,(A1;A5; A19;A34) 或 (A1;A2;A3;...;A34) 但如果我 select (A1:A34).

问题

我应该如何编写我的函数,以便我可以 select 我想要的任何单元格,然后使用它们的内容进行计算?

我终于弄清楚了如何对数据进行排序,以便该函数可以处理连续和不连续的范围,非常感谢 SJR 和 Ralph 以及 [=14] 对问题的评论=].

允许不连续范围的方法是使用 ParamArray,然后遍历所有输入的参数并检查它们包含的内容(这是我最初失败的地方,因为我不知道如何让 Excel 检查我提供给函数的每个参数的内容)。棘手的部分是,如果它当前检查的参数仅包含一个单元格,那么它需要处理的方式与包含连续范围时的处理方式不同。

例如,如果对仅包含一个单元格的参数使用 UBound,则检查 ParamArray 中的所有参数将失败。此外,为了正确寻址参数中连续范围内的每个单元格,需要循环遍历 InputParameters(i).Cells(j),而如果参数只是单个单元格,则足以寻址它作为 InputParameters(i).

我生成的代码现在可以正常工作了;我可以 select 任意范围的单元格并计算标准偏差和平均值,同时排除 NaN 值。我将它与内置的 STDEV.S、STDEV.P 和 AVERAGE 进行了比较,结果完全相同*。我不知道为什么内置函数不将 NaN 值排除为默认值,但我为任何想要使用它的人提供了下面函数的代码。

STDEV.S 的代码,不包括 NaN 值

Function NaNStdev_S(ParamArray xRange() As Variant) As Double
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'A function to calculate the sample standard deviation of any ranges of cells
'while excluding text, logicals, empty cells and cells containing #N/A.
'Can handle both continuous and discontinuous ranges.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Dim CellsUsed As Integer
Dim NumArg As Integer
Dim NumCell As Integer
Dim xAvg As Double
Dim xSum As Double
Dim xTemp As Variant
Dim xVect() As String

NumArg = UBound(xRange) 'Counts the number of input arguments (i.e., number of discontinuous regions)

For i = 0 To NumArg 'Goes through each discontinuous region
    xTemp = xRange(i) 'Stores the current region in a temporary variable as several of the later operations cannot be performed on the full input array
    If IsArray(xTemp) Then 'Checks if the current region is an array; if yes, then that array will be continuous
        NumCell = UBound(xTemp, 1) * UBound(xTemp, 2) 'Checks how many cells are in the array
            For j = 1 To NumCell 'Goes through all cells in the current region
                If IsEmpty(xRange(i).Cells(j)) Then 'do nothing
                ElseIf Application.IsLogical(xRange(i).Cells(j)) Then 'do nothing
                ElseIf IsNumeric(xRange(i).Cells(j)) Then 'If the content of the cell is numeric, then use it
                    xSum = xSum + xRange(i).Cells(j) 'Add the current cell value to the sum of all cell values
                    CellsUsed = CellsUsed + 1 'Counts how many of the cell values that are actually used
                    ReDim Preserve xVect(CellsUsed) 'Adjusts the size of xVect
                    xVect(CellsUsed) = xRange(i).Cells(j) 'Reformats all usable values into one single vector for later use
                Else
                End If
            Next j
    Else 'If the current region is not an array, then it's just a single value
        If IsEmpty(xRange(i)) Then 'do nothing
        ElseIf IsNumeric(xRange(i)) Then 'If the content of the current region is numeric, then use it
            xSum = xSum + xRange(i) 'Add the current cell (region) value to the sum of all cell values
            CellsUsed = CellsUsed + 1 'Increase the counter of used values
            ReDim Preserve xVect(CellsUsed) 'Adjusts the size of xVect
            xVect(CellsUsed) = xRange(i) 'Adds the current value into the reformatted vector for later use
        Else
        End If
    End If
Next i

xAvg = xSum / CellsUsed 'Average of all cells which contains numbers

xSum = 0 'resets the sum as it's no longer needed
For i = 1 To CellsUsed 'Goes through the reformatted vector and calculates the sum of (x - x_avg) ^ 2
    xSum = xSum + (xVect(i) - xAvg) ^ 2 'This is the dividend of the variance equation
Next i

NaNStdev_S = (xSum / (CellsUsed - 1)) ^ 0.5 'the sample standard deviation is the square root of the corrected variance

End Function

STDEV.P 的代码,不包括 NaN 值

Function NaNStdev_P(ParamArray xRange() As Variant) As Double
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'A function to calculate the population standard deviation of any ranges of cells
'while excluding text, logicals, empty cells and cells containing #N/A.
'Can handle both continuous and discontinuous ranges.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Dim CellsUsed As Integer
Dim NumArg As Integer
Dim NumCell As Integer
Dim xAvg As Double
Dim xSum As Double
Dim xTemp As Variant
Dim xVect() As String

NumArg = UBound(xRange) 'Counts the number of input arguments (i.e., number of discontinuous regions)

For i = 0 To NumArg 'Goes through each discontinuous region
    xTemp = xRange(i) 'Stores the current region in a temporary variable as several of the later operations cannot be performed on the full input array
    If IsArray(xTemp) Then 'Checks if the current region is an array; if yes, then that array will be continuous
        NumCell = UBound(xTemp, 1) * UBound(xTemp, 2) 'Checks how many cells are in the array
            For j = 1 To NumCell 'Goes through all cells in the current region
                If IsEmpty(xRange(i).Cells(j)) Then 'do nothing
                ElseIf Application.IsLogical(xRange(i).Cells(j)) Then 'do nothing
                ElseIf IsNumeric(xRange(i).Cells(j)) Then 'If the content of the cell is numeric, then use it
                    xSum = xSum + xRange(i).Cells(j) 'Add the current cell value to the sum of all cell values
                    CellsUsed = CellsUsed + 1 'Counts how many of the cell values that are actually used
                    ReDim Preserve xVect(CellsUsed) 'Adjusts the size of xVect
                    xVect(CellsUsed) = xRange(i).Cells(j) 'Reformats all usable values into one single vector for later use
                Else
                End If
            Next j
    Else 'If the current region is not an array, then it's just a single value
        If IsEmpty(xRange(i)) Then 'do nothing
        ElseIf IsNumeric(xRange(i)) Then 'If the content of the current region is numeric, then use it
            xSum = xSum + xRange(i) 'Add the current cell (region) value to the sum of all cell values
            CellsUsed = CellsUsed + 1 'Increase the counter of used values
            ReDim Preserve xVect(CellsUsed) 'Adjusts the size of xVect
            xVect(CellsUsed) = xRange(i) 'Adds the current value into the reformatted vector for later use
        Else
        End If
    End If
Next i

xAvg = xSum / CellsUsed 'Average of all cells which contains numbers

xSum = 0 'resets the sum as it's no longer needed
For i = 1 To CellsUsed 'Goes through the reformatted vector and calculates the sum of (x - x_avg) ^ 2
    xSum = xSum + (xVect(i) - xAvg) ^ 2 'This is the dividend of the variance equation
Next i

NaNStdev_P = (xSum / CellsUsed) ^ 0.5  'the population standard deviation is the square root of the variance

End Function

排除 NaN 值的 AVERAGE 代码

Function NaNAverage(ParamArray xRange() As Variant) As Double
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'A function to calculate the average of any ranges of cells
'while excluding text, logicals, empty cells and cells containing #N/A.
'Can handle both continuous and discontinuous ranges.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Dim CellsUsed As Integer
Dim NumArg As Integer
Dim NumCell As Integer
Dim xSum As Double
Dim xTemp As Variant

NumArg = UBound(xRange) 'Counts the number of input arguments (i.e., number of discontinuous regions)

For i = 0 To NumArg 'Goes through each discontinuous region
    xTemp = xRange(i) 'Stores the current region in a temporary variable as several of the later operations cannot be performed on the full input array
    If IsArray(xTemp) Then 'Checks if the current region is an array; if yes, then that array will be continuous
        NumCell = UBound(xTemp, 1) * UBound(xTemp, 2) 'Checks how many cells are in the array
            For j = 1 To NumCell 'Goes through all cells in the current region
                If IsEmpty(xRange(i).Cells(j)) Then 'do nothing
                ElseIf Application.IsLogical(xRange(i).Cells(j)) Then 'do nothing
                ElseIf IsNumeric(xRange(i).Cells(j)) Then 'If the content of the cell is numeric, then use it
                    xSum = xSum + xRange(i).Cells(j) 'Add the current cell value to the sum of all cell values
                    CellsUsed = CellsUsed + 1 'Counts how many of the cell values that are actually used
                Else
                End If
            Next j
    Else 'If the current region is not an array, then it's just a single value
        If IsEmpty(xRange(i)) Then 'do nothing
        ElseIf IsNumeric(xRange(i)) Then 'If the content of the current region is numeric, then use it
            xSum = xSum + xRange(i) 'Add the current cell (region) value to the sum of all cell values
            CellsUsed = CellsUsed + 1 'Increase the counter of used values
        Else
        End If
    End If
Next i

NaNAverage = xSum / CellsUsed 'Average of all cells which contains numbers

End Function

*免责声明

我提到代码产生的值与内置函数完全相同 - 但是,我确实注意到有一次它没有产生。 我将以下随机选择的值作为随机大小和定位的范围放置在我的 Excel sheet:

(00:01:00, -10, -33, 10, 33, 20, 66, 30, 40, 300, TRUE, {empty cell} , #N/A)

如果它们是随机分布的(即,我将它们放在以下单元格中(P22:Q23;R22:R23;S22:T22;S21:V21;Q28)),那么它们与 STDEV.S 产生(我从 STDEV.S 函数中手动排除了带有 #N/A 的单元格),但它们仅在小数点后第 13 位不同(我的函数给出 93.5950714912684,而 STDEV.S 给出 93.5950714912683 ), 这应该是一个足够小的错误以至于无关紧要。有趣的是,如果我将所有值放在一行中(即,我将所有值放在例如 (M34:Y34) 上),那么我的函数和内置函数都会给出完全相同的结果(即 93.5950714912683) .错误似乎源于包含 1 分钟的单元格;如果我将 00:01:00 更改为任何其他时间值(例如 00:01:01 或 01:01:00),那么无论这些值是放在一行中还是随机分布,这两个函数都会产生完全相同的结果工作区域sheet。

我无法解释这种奇怪的行为,但到目前为止它似乎只产生了一个微不足道的错误,所以我假设我的代码按预期工作。