Histogrammer i Excel med VBA-makroer
På denne side viser jeg, hvordan man hurtigt og nemt kan lave histogrammer i Excel med lidt VBA-kode (makroer). Jeg viser også, hvordan man kan lave en normalfordelt, klokkeformet kurve over søjlediagrammet.
Et histogram viser grafisk hyppigheden af forekomster i et datasæt, enten som frekvenser eller i procent, og man kan visuelt vurdere, om data er normalfordelt.
Vil man være sikker på det med normalfordelingen, må man dog lave en statistisk test, men det vil jeg ikke komme ind på - emnet her er VBA og ikke statistik.
I Excel kan man lave histogrammer manuelt, hvis man har installeret tilføjelsesprogrammet Analysis ToolPak, men jeg synes, det er bøvlet (se her hvordan), hvis man skal gøre det ofte på datasæt af forskellig størrelse.
En makro i VBA er derimod lynhurtig. Jeg viser først, hvordan man kan lave et histogram som i Microsofts eksempel, blot med makroer i stedet.
Derefter viser jeg (link), hvordan man ud fra et brugervalgt antal søjler beregner intervalstørrelsen, finder hyppigheden af forekomster i hvert interval, omregner hyppigheden til procent og indsætter det i et histogram.
Du kan markere og kopiere koden til et VBA-modul eller downloade et zip-komprimeret regneark med eksemplerne. Regnearket (og koden herunder) antager, at kildedata til histogrammet er i et andet regneark, og histogrammet indsættes på et nyt faneblad i arket med kildedata.
Histogram med klokkeformet kurve
I det første histogram er antallet af søjler (intervaller) fast, nemlig 8, og intervallernes størrelse beregnes ud fra datasættets standardafvigelse og middelværdi.
Til at lave den klokkeformede, normalfordelte kurve bruger vi 2000 "tilfældige" tal med (næsten) samme standardafvigelse og middelværdi. Fidusen til at gøre dette er at bruge regnearksfunktionen "NormInv" sammen med VBA's generator af tilfældige tal.
På modulniveau skal du deklarere 2 public variable: Public bAbort as Boolean og Public bBell as Boolean.
Sub BellShape()
bBell = True
OpenForm
If bAbort Then
bAbort = False
Exit Sub
End If
End Sub
Sub OpenForm()
On Error GoTo ErrorHandle
If Workbooks.Count = 1 Then
With Application.FileDialog(msoFileDialogOpen)
.Title = "Åbn ark med data til histogrammet"
.AllowMultiSelect = False
.Filters.Add "Workbooks", "*.xl*", 1
.Show
If .SelectedItems.Count > 0 Then
Workbooks.Open (.SelectedItems(1))
Else
bAbort = True
Exit Sub
End If
End With
ElseIf Workbooks.Count = 2 Then
If Workbooks.Item(1).Name = ThisWorkbook.Name Then
Workbooks(2).Activate
Else
Workbooks(1).Activate
End If
ElseIf Workbooks.Count > 2 Then
Workbooks(Workbooks.Count).Activate
frmPickSheet.Show vbModeless
Exit Sub
ErrorHandle:
MsgBox Err.Description & " Procedure OpenForm"
End Sub
Du skal lave en simpel Userform og kalde den "frmPickSheet". På formularen skal være en label og 2 commaand buttons. På labelen beder du brugeren aktivere fanebladet med kildedata, og formularens kode skal bare være som følger:
Private Sub CommandButton1_Click()
Unload Me
If bBell Then
Module1.Histogram2
Else
Module1.Histogram
End If
End Sub
Private Sub CommandButton2_Click()
Unload Me
End Sub
Så vidt, så godt. Hvis brugeren ikke har annulleret, er regnearket med kildedata nu det aktive ark, og vi kan gå videre. I den næste procedure beder vi brugeren om at vælge den øverste celle/værdi i kolonnen med kildedata.
Vi kontrollerer, at data er valide, og hvis alt er OK, laver vi histogrammet.
Sub Histogram2()
Dim bOK As Boolean
Dim dAvg As Double
Dim dStdev As Double
Dim dStep As Double
Dim dMax As Double
Dim dRangeMax As Double
Dim dMin As Double
Dim lCount As Long
Dim lLast As Long
Dim rCell As Range
Dim rInput As Range
Dim rBell As Range
Dim arData(1 To 8)
Dim arBackup()
Dim arIntervals(1 To 8, 1 To 2)
On Error Resume Next
Set rInput = Application.InputBox _
(prompt:="Markér øverste celle i kolonnen med " & _
"værdier, som skal bruges i histogrammet.", Type:=8)
If rInput Is Nothing Then Exit Sub
On Error GoTo ErrorHandle
With rInput
If .Count > 1 Then
MsgBox "Der skal kun vælges 1 celle."
ThisWorkbook.Activate
GoTo BeforeExit
End If
If Len(.Value) = 0 Then
MsgBox "Cellen er tom."
GoTo BeforeExit
End If
If IsNumeric(.Value) = False Then
If IsNumeric(.Offset(1, 0)) = False Then
MsgBox "Det skal være et tal"
ThisWorkbook.Activate
GoTo BeforeExit
Else
Set rInput = rInput.Offset(1, 0)
End If
End If
If IsEmpty(.Offset(1, 0)) Then
MsgBox "Der skal mere end 1 værdi til at lave et histogram!"
GoTo BeforeExit
End If
End With
Set rInput = Range(rInput, rInput.End(xlDown))
For Each rCell In rInput
If IsNumeric(rCell.Value) = False Then
MsgBox "Værdien i celle " & rCell.Address & _
" er ikke numerisk."
rCell.Select
GoTo BeforeExit
End If
Next
On Error GoTo ErrorHandle
Application.ScreenUpdating = False
arBackup = rInput.Value
dAvg = WorksheetFunction.Average(rInput)
dStdev = WorksheetFunction.StDev(rInput)
dMin = dAvg - 3 * dStdev
dRangeMax = WorksheetFunction.Max(rInput) + 1
dStep = dStdev
dMax = dMin + 6 * dStdev
arIntervals(1, 2) = dMin
For lCount = 2 To 8
arIntervals(lCount, 1) = arIntervals(lCount - 1, 2)
If lCount < 8 Then
arIntervals(lCount, 2) = arIntervals(lCount - 1, 2) + dStep
Else
arIntervals(8, 2) = dRangeMax
End If
Next
rInput.Sort Key1:=rInput.Item(1), _
Order1:=xlAscending, Header:=xlGuess, _
OrderCustom:=1, MatchCase:=False, _
Orientation:=xlTopToBottom, _
DataOption1:=xlSortNormal
lLast = 1
For Each rCell In rInput
With rCell
For lCount = lLast To 8
If lCount = 1 Then
If .Value < arIntervals(lCount, 2) Then
lLast = lCount
End If
Else
If .Value < arIntervals(lCount, 2) _
And .Value >= arIntervals(lCount, 1) Then
lLast = lCount
Exit For
End If
End If
Next
If lCount = 9 Then lCount = 8
arData(lCount) = arData(lCount) + 1
End With
Next
For lCount = 1 To 8
If Len(arData(lCount)) = 0 Then
arData(lCount) = 0
End If
Next
Sheets.Add , Worksheets.Item(Sheets.Count)
Set rCell = Range("B2")
For lCount = 0 To 7
With rCell
.Offset(lCount).Value = _
arData(lCount + 1) * 100 / rInput.Count
.Offset(lCount, 1).Value = arData(lCount + 1)
If lCount < 7 Then
.Offset(lCount, -1).Value = arIntervals(lCount + 1, 2)
Else
.Offset(lCount, -1).Value = "Mere"
End If
End With
Next
Range("A1").Value = "Intervaller"
Range("B1").Value = "Procent"
Range("C1").Value = "Frekvens"
Range("f1").Value = "Snit"
Range("f2").Value = "Stdafv."
Range("f3").Value = "Max"
Range("f4").Value = "Min"
Range("g1").Value = WorksheetFunction.Average(rInput)
Range("g2").Value = WorksheetFunction.StDev(rInput)
Range("g3").Value = dMax
Range("g4").Value = dMin
Range("g1:g4").NumberFormat = "#0.00"
Range("A1:B8").NumberFormat = "#0.00"
Columns("A:A").EntireColumn.AutoFit
RandomNumbers dAvg, dStdev
Set rBell = Range("M1")
Set rBell = Range(rBell, rBell.End(xlDown))
rBell.Sort Key1:=rBell.Item(1), _
Order1:=xlAscending, Header:=xlGuess, _
OrderCustom:=1, MatchCase:=False, _
Orientation:=xlTopToBottom, _
DataOption1:=xlSortNormal
For lCount = 1 To 8
arData(lCount) = 0
Next
lLast = 1
For Each rCell In rBell
With rCell
For lCount = lLast To 8
If lCount = 1 Then
If .Value < arIntervals(lCount, 2) Then
lLast = lCount
Exit For
End If
Else
If .Value < arIntervals(lCount, 2) _
And .Value >= arIntervals(lCount, 1) Then
lLast = lCount
Exit For
End If
End If
Next
If lCount = 9 Then lCount = 8
arData(lCount) = arData(lCount) + 1
End With
Next
Set rCell = Range("e2")
For lCount = 0 To 7
With rCell
.Offset(lCount, 0).Value = arData(lCount + 1)
If lCount < 7 Then
.Offset(lCount, -1).Value = arIntervals(lCount + 1, 2)
Else
.Offset(lCount, -1).Value = "Mere"
End If
End With
Next
MakeChart
BeforeExit:
On Error Resume Next
rInput.Value = arBackup
Set rBell = Nothing
Set rCell = Nothing
Set rInput = Nothing
Erase arData
Erase arBackup
Erase arIntervals
Application.ScreenUpdating = True
Exit Sub
ErrorHandle:
MsgBox Err.Description
Resume BeforeExit
End Sub
Sub RandomNumbers(ByVal dAvg As Double, ByVal dStdev As Double)
Dim lCount As Long
Dim dRandom As Double
Randomize
For lCount = 0 To 1999
dRandom = WorksheetFunction.NormInv(Rnd(), dAvg, dStdev)
Range("M1").Offset(lCount, 0).Value = dRandom
Next
End Sub
Sub MakeChart()
Dim sSheet As String
On Error GoTo ErrorHandle
sSheet = ActiveSheet.Name
Range("A1:A9,C1:C9,E1:E9").Select
Charts.Add
ActiveChart.ChartType = xlXYScatterSmoothNoMarkers
ActiveChart.SetSourceData Source:= _
Sheets(sSheet).Range("A1:A9,C1:C9,E1:E9")
ActiveChart.Location Where:=xlLocationAsObject, Name:=sSheet
ActiveChart.SeriesCollection(1).AxisGroup = 2
ActiveChart.SeriesCollection(1).ChartType = xlColumnClustered
ActiveChart.SeriesCollection(1).Select
With ActiveChart.ChartGroups(1)
.Overlap = 0
.GapWidth = 0
.HasSeriesLines = False
.VaryByCategories = False
End With
ActiveChart.PlotArea.Select
With Selection.Border
.ColorIndex = 16
.Weight = xlThin
.LineStyle = xlContinuous
End With
With Selection.Interior
.ColorIndex = 2
.PatternColorIndex = 1
.Pattern = xlSolid
End With
ActiveChart.SeriesCollection(2).Select
With Selection.Border
.ColorIndex = 57
.Weight = xlThick
.LineStyle = xlContinuous
End With
With Selection
.MarkerBackgroundColorIndex = xlNone
.MarkerForegroundColorIndex = xlNone
.MarkerStyle = xlNone
.Smooth = True
.MarkerSize = 3
.Shadow = False
End With
ActiveChart.Legend.Delete
Exit Sub
ErrorHandle:
MsgBox Err.Description & " Procedure MakeChart"
End Sub
Det var det første histogram med klokkeformet kurve ovenover, ganske som i eksemplet på Microsofts supportside, men uden manuelt og besværligt fnidder og uden brug af tilføjelsesprogrammet Analysis ToolPak.
Histogram med brugervalgt antal søjler
I eksemplet ovenover opererede vi med et fast antal søjler, og som intervalstørrelse brugte vi datasættets standardafvigelse. I eksemplet herunder lader vi brugeren bestemme antallet af søjler (intervaller), og intervalstørrelsen beregnes som:
(største værdi i datasættet - mindste værdi) / antal søjler
Derefter finder vi hyppigheden i hvert interval og laver histogrammet som et simpelt søjlediagram.
Til at identificere regnearket med kildedata bruges samme OpenForm-procedure som ovenfor, så den viser jeg ikke igen.
Sub Simple()
bBell = False
OpenForm
If bAbort Then
bAbort = False
Exit Sub
End If
End Sub
Sub Histogram()
Dim bOK As Boolean
Dim rCell As Range
Dim rInput As Range
Dim dMax As Double
Dim dMin As Double
Dim dStep As Double
Dim lCount As Long
Dim lLast As Long
Dim sInterval As String
Dim arData()
Dim arBackup()
Dim vInput
Dim sSheet As String
On Error Resume Next
Set rInput = Application.InputBox(prompt:= _
"Markér øverste celle i kolonnen med " & _
"værdier, som skal bruges i histogrammet.", Type:=8)
If rInput Is Nothing Then Exit Sub
On Error GoTo ErrorHandle
With rInput
If .Count > 1 Then
MsgBox "Der skal kun vælges 1 celle."
ThisWorkbook.Activate
GoTo BeforeExit
End If
If Len(.Value) = 0 Then
MsgBox "Cellen er tom."
GoTo BeforeExit
End If
If IsNumeric(.Value) = False Then
If IsNumeric(.Offset(1, 0)) = False Then
MsgBox "Det skal være et tal"
ThisWorkbook.Activate
GoTo BeforeExit
Else
Set rInput = rInput.Offset(1, 0)
End If
End If
If IsEmpty(.Offset(1, 0)) Then
MsgBox "Der skal mere end 1 værdi til at lave et histogram."
GoTo BeforeExit
End If
End With
Set rInput = Range(rInput, rInput.End(xlDown))
For Each rCell In rInput
If IsNumeric(rCell.Value) = False Then
MsgBox "Værdien i celle " & rCell.Address & _
" er ikke numerisk."
rCell.Select
GoTo BeforeExit
End If
Next
Do Until bOK
vInput = InputBox("Hvor mange søjler/intervaller " & _
"skal der være i histogrammet?", "Antal søjler")
If vInput = vbCancel Then
ThisWorkbook.Activate
GoTo BeforeExit
ElseIf Len(vInput) = 0 Or IsNumeric(vInput) = False Then
MsgBox "Antal søjler skal være et tal"
ElseIf vInput < 3 Then
MsgBox vInput & " søjler giver ingen mening til et histogram."
ElseIf vInput > rInput.Count Then
MsgBox "Der er flere intervaller end værdier"
Else
bOK = True
End If
Loop
On Error GoTo ErrorHandle
Application.ScreenUpdating = False
ReDim arData(1 To vInput)
arBackup = rInput.Value
dMax = WorksheetFunction.Max(rInput)
dMin = WorksheetFunction.Min(rInput)
dStep = (dMax - dMin) / vInput
rInput.Sort Key1:=rInput.Item(1), _
Order1:=xlAscending, Header:=xlGuess, _
OrderCustom:=1, MatchCase:=False, _
Orientation:=xlTopToBottom, _
DataOption1:=xlSortNormal
lLast = 2
For Each rCell In rInput
With rCell
If .Value < dMin + dStep Then
lCount = 1
Else
For lCount = lLast To vInput
If .Value < dMin + dStep * lCount Then
lLast = lCount
Exit For
End If
Next
End If
If lCount = vInput + 1 Then lCount = vInput
arData(lCount) = arData(lCount) + 1
End With
Next
Sheets.Add , Worksheets.Item(Sheets.Count)
Set rCell = Range("B2")
For lCount = 0 To vInput - 1
rCell.Offset(lCount).Value = _
Round(arData(lCount + 1) * 100 / rInput.Count, 2)
If lCount + 1 < UBound(arData) Then
sInterval = Str(Round(dMin + dStep * lCount, 2)) & _
"-" & Str(Round(dMin + dStep * (lCount + 1) - 0.01, 2))
rCell.Offset(lCount, 1).Value = arData(lCount + 1)
Else
sInterval = Str(Round(dMin + dStep * lCount, 2)) & _
"-" & Str(Round(dMax, 2))
rCell.Offset(lCount, 1).Value = arData(lCount + 1)
End If
rCell.Offset(lCount, -1).Value = sInterval
Next
Range("A1").Value = "Intervaller"
Range("B1").Value = "Procent"
Range("C1").Value = "Frekvens"
Range("E1").Value = "Snit"
Range("E2").Value = "Stdafv."
Range("E3").Value = "Max"
Range("E4").Value = "Min"
Range("F1").Value = WorksheetFunction.Average(rInput)
Range("F2").Value = WorksheetFunction.StDev(rInput)
Range("F3").Value = dMax
Range("F4").Value = dMin
Range("F1:F4").NumberFormat = "#0.00"
Columns("A:A").EntireColumn.AutoFit
sSheet = ActiveSheet.Name
Set rCell = Range("A1:B" & vInput + 1)
Charts.Add
ActiveChart.ChartType = xlColumnClustered
ActiveChart.SetSourceData Source:=rCell, PlotBy:= _
xlColumns
ActiveChart.Location Where:=xlLocationAsObject, Name:=sSheet
With ActiveChart
.HasTitle = True
.ChartTitle.Characters.Text = "Procent"
.Axes(xlCategory, xlPrimary).HasTitle = False
.Axes(xlValue, xlPrimary).HasTitle = False
.HasLegend = False
End With
With ActiveChart.ChartGroups(1)
.Overlap = 0
.GapWidth = 0
.HasSeriesLines = False
.VaryByCategories = False
End With
BeforeExit:
On Error Resume Next
rInput.Value = arBackup
Set rCell = Nothing
Set rInput = Nothing
Erase arData
Erase arBackup
Application.ScreenUpdating = True
Exit Sub
ErrorHandle:
MsgBox Err.Description & " Procedure Histogram"
Resume BeforeExit
End Sub
Det var det: To typer histogram med VBA makroer.
|