Centrum- en spredingsmaten#

Samenvatting#

In dit deel spreken we over centrum -en spreidingsmaten. Centrummaten proberen het “midden” van de gemeten data weer te geven, terwijl spreidingsmaten dan weer een idee proberen te geven van hoe de gegevens verspreid liggen rond dit midden.

De ruwe gegevens#

In dit deeltje werken we met dezelfde ruwe gegevens als in het deeltje over frequenties. Meer informatie vind je daar. We lezen de tabel met de laptopgegevens opnieuw in en voeren de benodigde datamanagementstappen uit om de data om te kuisen.

import pandas as pd

laptops = pd.read_csv('datasets/laptops.csv', sep=';', decimal=',')
# ordinaal niveau maken
cpuGenerationLevels = ['Sandy Bridge', 'Ivy Bridge', 'Haswell', 'Broadwell', 'Skylake', 'Kabylake']
laptops.cpuGeneration = pd.Categorical(laptops.cpuGeneration, ordered=True, categories=cpuGenerationLevels)

# ordinaal niveau maken
cpuTypeLevels = ['i3', 'i5', 'i7']
laptops.cpuType = pd.Categorical(laptops.cpuType, ordered=True, categories=cpuTypeLevels)

# optioneel: nominaal niveau maken (bespaart geheugen)
laptops.brand = pd.Categorical(laptops.brand)
laptops.head()
cpuGeneration cpuType RAM diskspace brand
0 Kabylake i7 4.0 232.5 Toshiba
1 Kabylake i5 2.0 992.5 Acer
2 Haswell i7 16.0 495.6 Dell
3 Skylake i7 4.0 217.2 Toshiba
4 Broadwell i5 4.0 245.8 Acer

Centrummaten#

Centrummaten proberen het “midden” van de gemeten data weer te geven. Het gemiddelde is een heel bekende centrummaat, maar er zijn er ook andere die in bepaalde omstandigheden interessanter kunnen zijn. Je kan de centrummaten berekenen uit ruwe data, maar ook uit een frequentietabel. Dit wordt ook iedere keer gedemonstreerd.

Modus#

Als je variabele nominaal meetniveau heeft, dan kan je er niet mee rekenen. Een gemiddelde bepalen heeft dus helemaal geen zin, zelfs niet wanneer je getallen toewijst aan de mogelijke waarden. Een voorbeeldje is de burgerlijke staat van een persoon. Deze kan zijn: “alleenstaand”, “gehuwd”, “samenwonend” of “gescheiden”. Het heeft geen enkele zin om de gemiddelde burgerlijke staat te berekenen.

Wat we wel kunnen doen, is kijken welke waarde het meest voorkomt. Dit noemt men de modus. Als we bijvoorbeeld willen weten welke cpu het meest voorkomt in onze dataset, dan kan je dit met de functie mode() vinden:

laptops.cpuGeneration.mode()
0    Broadwell
Name: cpuGeneration, dtype: category
Categories (6, object): ['SandyBridge' < 'IvyBridge' < 'Haswell' < 'Broadwell' < 'Skylake' < 'Kabylake']

De meest voorkomende cpu in de gemeten laptops is dus Broadwell. Dat is een Intel Core processor van de 5e generatie.

De voordelen van een modus zijn:

  • altijd te bepalen (vanaf nominaal meetniveau)

  • eenvoudig te bepalen (aan de hand van de frequenties)

  • heel ongevoelig voor uitschieters (zie ook verder in dit document)

Er zijn echter ook nadelen:

  • soms moet je opdelen in klassen om het aantal mogelijkheden te beperken. Stel dat je de modus wil bepalen van de vrije schijfruimte, dan ga je bijvoorbeeld als volgt te werk:

cutpoints = [0, 185, 375, 750, 1100]
klassen = pd.cut(laptops.diskspace, bins=cutpoints)
klassen.cat.categories = [120, 250, 500, 1000]
klassen.mode()
0    250
Name: diskspace, dtype: category
Categories (4, int64): [120 < 250 < 500 < 1000]
  • heel afhankelijk van de opdeling in mogelijke waarden. Als je bijvoorbeeld bij burgerlijke staat de waarden “samenwonend” en “gehuwd” samen neemt, dan zal de frequentie van deze groep verhogen en de modus dus misschien verschuiven. Als je andere klassen definieert in bovenstaand voorbeeld (met de vrije schijfruimte), dan zal je ook een andere modus kunnen vinden.

  • slechts 1 waarde doet mee in het uiteindelijke resultaat. Variaties in andere waarden hebben geen enkele invloed. Dat is tegelijk ook een voordeel (bestand tegen uitschieters)

  • soms heb je meer dan 1 modus. Daarom dat het resultaat in Python een Series is.

Mediaan#

Een van de meest robuuste manieren om een centrum te vinden, is de mediaan. Je kan een mediaan berekenen vanaf ordinaal meetniveau. De werkwijze is als volgt: sorteer de waarden van klein naar groot (vandaar dat ze ordinaal moeten zijn) en neem de middelste waarde. Indien er 2 middelste waarden zijn (er zijn een even aantal waarden in totaal), dan zijn er 2 mogelijkheden: ofwel kies je 1 van deze 2, ofwel neem je het gemiddelde van deze 2 (de variabele moet dan wel minstens interval meetniveau hebben).

Het effect van deze berekening, is dat steeds de helft van de gegevens kleiner zal zijn dan de mediaan en de helft groter.

In Python kan je de mediaan bepalen met de functie median(), maar deze werkt enkel met getallen. Deze functie houdt standaard geen rekening met ontbrekende waarden:

laptops.RAM.median()
4.0

Als we echter de mediaan van cpuGeneration willen berekenen, geeft Python een foutmelding:

laptops.cpuGeneration.median()

Error

TypeError: ‘Categorical’ with dtype category does not support reduction ‘median’

Dit komt omdat je soms het gemiddelde moet berekenen (bij een even aantal waarden) en dat gaat niet bij kwalitatieve variabelen. We kunnen echter kiezen om de laagste waarde te nemen. Daarvoor moeten we zelf een functie schrijven:

import math


def median_categorical(data):
    data_no_na = data.dropna()
    n = len(data_no_na)
    middle = math.floor(n / 2)
    return data_no_na.sort_values().reset_index(drop=True)[middle]

Deze functie haalt de NaN-waarden eerst weg. Dan wordt de positie van het midden van de data berekend. De functie floor() zorgt ervoor dat we de eerste waarde kiezen indien er een even aantal waarden zijn. Tenslotte worden alle waarden gesorteerd en de middelste waarde geselecteerd. De functie reset_index() is belangrijk omdat sort() de indexen mee sorteert en dat willen we hier niet (we resetten deze terug van 0 tot n-1). De parameter drop=True zorgt ervoor dat de originele index ook verwijderd wordt (anders wordt die opgenomen als een extra kolom)

Je moet dus altijd goed kijken welk meetniveau je variabele heeft. Afhankelijk daarvan bereken je de mediaan op een andere manier. Je kan zo bijvoorbeeld de mediaan berekenen van RAM en cpuGeneration:

laptops.RAM.median()
median_categorical(laptops.cpuGeneration)

Bemerk dat het geen zin heeft om de mediaan te berekenen van het merk van de laptops. Er is namelijk geen enkele zinvolle manier om deze waarden te sorteren. De hierboven beschreven functie zal wel een antwoord geven, maar dit heeft geen enkele betekenis:

median_categorical(laptops.brand)

Als gegevens uit een stream komen, en je wil steeds de mediaan berekenen, dan zal dit op den duur enorm veel rekenwerk vragen. Je moet de gegevens toch iedere keer weer sorteren. Daarvoor moet je de steeds groeiende lijst ook bijhouden. Op deze manier verspil je veel geheugen en processortijd. Er bestaat dan echter een manier om dit te versnellen. In plaats van de gegevens zelf bij te houden, houden we een frequentietabel bij. Die is heel gemakkelijk aan te passen als er nieuwe data binnen komt. Een voorwaarde is wel dat er niet te veel verschillende waarden mogen zijn, want anders zal de frequentietabel ook heel groot worden. Nu moeten we enkel nog de mediaan vinden vanuit een frequentietabel.

Dat kan als volgt: stel dat we vertrekken vanuit de relatieve frequenties van cpuGeneration:

cumul_percentages = laptops.cpuGeneration.value_counts(normalize=True).sort_index().cumsum() * 100
cumul_percentages
Sandy Bridge      7.394366
Ivy Bridge       19.953052
Haswell          39.436620
Broadwell        65.023474
Skylake          83.215962
Kabylake        100.000000
Name: cpuGeneration, dtype: float64

Als we nu de mediaan willen berekenen, zoeken we de waarde waarbinnen 50% valt (de eerste waarde die een cumulatief percentage van 50 of meer heeft):

cumul_percentages[cumul_percentages > 50]
Broadwell     65.023474
Skylake       83.215962
Kabylake     100.000000
Name: cpuGeneration, dtype: float64
cumul_percentages[cumul_percentages > 50].index[0]
'Broadwell'

We vinden inderdaad weer “Broadwell” als mediaan.

Voordelen van de mediaan zijn:

  • je kan de mediaan berekenen vanaf ordinaal meetniveau

  • de mediaan is ongevoelig voor extreme waarden (die worden door het sorteren links en rechts geplaatst en zullen het midden dus nooit beïnvloeden)

  • als er heel veel verschillende waarden zijn, hoef je geen klassen te maken

Er zijn natuurlijk ook nadelen:

  • de mediaan wordt niet bepaald door de exacte getalwaarden van alle gegevens. Enkel de volgorde is van belang. Variaties in andere waarden hebben geen enkele invloed op de mediaan, tenzij de volgorde daardoor sterk wisselt (maar daardoor is de mediaan niet gevoelig voor uitschieters, wat weer een voordeel is).

  • om de mediaan te berekenen, moet je de waarden sorteren. Dit vraagt veel tijd.

  • je kan de mediaan niet incrementeel berekenen aan de hand van de vorige mediaan en een nieuw gegeven. Dat is vervelend als je veel data moet verwerken waarbij er steeds nieuwe data bijkomt. Je moet alle data in principe altijd bijhouden. Als er maar een beperkt aantal waarden mogelijk zijn, is er wel een oplossing. Dan hou je een tabel met de absolute frequenties bij en bereken je de mediaan zoals hierboven beschreven staat.

  • er is geen formule om de mediaan te berekenen en dus is het moeilijk om wiskundig bepaalde eigenschappen aan te tonen

Rekenkundig gemiddelde#

Het (rekenkundig) gemiddelde is de meest bekende centrummaat. Je kan deze echter pas berekenen vanaf interval meetniveau. Dat komt omdat we alle waarden moeten kunnen optellen. Het gemiddelde vind je door alle waarden op te tellen en de som te delen door het aantal elementen. De formule is als volgt:

x=1ni=1nxi

Note

We noteren het gemiddelde van een steekproef x altijd met x.

In Python kan je het gemiddelde als volgt berekenen (de NaN-waarden worden automatisch genegeerd):

laptops.diskspace.mean()
356.7618096357227

De gemiddelde vrije ruimte is dus ongeveer 356.8 GB.

Je kan een gemiddelde ook uit een frequentietabel berekenen, maar dit is enkel nuttig als het aantal mogelijke waarden beperkt is (anders heb je meer geheugen nodig voor de frequentietabel dan voor de data). Om dit te demonstreren, vertrekken we weer van een frequentietabel. Hier maken we die voor het RAM-geheugen:

abs_freq = laptops.RAM.value_counts().sort_index()
abs_freq
1.0      74
2.0     134
4.0     319
8.0     203
16.0    125
Name: RAM, dtype: int64

We vinden het gemiddelde door iedere waarde te vermenigvuldigen met de bijbehorende frequentie en dan te delen door de som van alle frequenties (dit laatste is het aantal elementen n).

In Python gaat dit zo:

import numpy as np

ram_waarden = abs_freq.index
np.average(ram_waarden, weights=abs_freq)
6.130994152046783

Tip

We gebruiken hiervoor de functie np.average van Numpy die zowel het gewone als het gewogen gemiddelde kan berekenen als je de weights-parameter meegeeft. In het volgende deeltje leer je wat een gewogen gemiddelde precies is.

Dit gewogen gemiddelde van de RAM-waarden (gewogen volgens hun absolute frequenties), is inderdaad dezelfde waarde wanneer we de ruwe data zouden gebruiken:

laptops.RAM.mean()
6.130994152046783

De voordelen van het gemiddelde zijn:

  • er is een eenvoudige formule. Daardoor kan je wiskundig eigenschappen over het gemiddelde gaan bewijzen. Dit is de reden waarom het gemiddelde veel gebruikt wordt: er is veel over geweten en je kan er duidelijke uitspraken over doen.

  • het gemiddelde gebruikt alle waarden. Het is dus een eigenschap waarin iedere waarde een rol speelt. Je krijgt dus informatie over alle waarden.

De nadelen zijn:

  • aangezien alle waarden een rol spelen, kunnen extreem grote of extreem kleine waarden het gemiddelde sterk beïnvloeden (zie uitschieters)

  • het gemiddelde kan een waarde zijn die niet kan voorkomen. Als je bijvoorbeeld het gemiddelde inkomen van een groep werknemers bepaalt, dan kan het zijn dat niemand van de onderzochte personen exact dat loon heeft. In het voorbeeld hierboven kan je ook zien dat er geen enkele laptop is die exact 357.5 GB schijfruimte vrij had. En geen enkele laptop heeft een RAM-geheugen van 6.13 GB.

Andere gemiddelden#

Buiten het rekenkundig gemiddelde zijn er ook andere gemiddelden. We bespreken ze hier kort.

Gewogen gemiddelde#

Soms wil je het gemiddelde berekenen waarbij sommige waarden meer “doorwegen” dan andere. Een voorbeeldje is het rapport van een student. Je ziet hier een voorbeeld:

vak

score

studiepunten

Computersystemen 1

18

6

OO programmeren 1

15

11

Datastr. en algoritmes

12

5

Data-Science 1

10

3

Netwerkarchitectuur 1

18

3

Communicatie 1

13

7

User interfaces 1

17

4

Databanken 1

15

7

Software engineering 1

13

7

Boekhouden

12

3

Management accounting

12

4

TOTAAL

???

60

We willen hier graag de gemiddelde score berekenen, maar rekening houdend met het aantal studiepunten. Een van van 6 studiepunten moet twee keer zo zwaar doorwegen als een vak van 3 studiepunten. De oplossing bestaat erin om alle scores met hun gewicht (hier studiepunten) te vermenigvuldigen. Je kan dan de som van al deze waarden berekenen en die delen door het totaal van alle gewichten. Dat noemt men het gewogen gemiddelde.

In Python kan je dit als volgt berekenen:

scores = [18, 15, 12, 10, 18, 13, 17, 15, 13, 12, 12]
studiepunten = pd.Series([6, 11, 5, 3, 3, 7, 4, 7, 7, 3, 4])
sum(studiepunten * scores) / sum(studiepunten)
14.266666666666667

Note

Bemerk dat we 1 van de 2 lists omzetten naar een Series zodat we de twee lijsten met elkaar kunnen vermenigvuldigen (standaard ondersteunt Python dit niet, maar Numpy & Pandas doen dit wel).

Een alternatieve manier om het gewogen gemiddelde te berekenen is om gebruik te maken van Numpy’s np.average.

scores = [18, 15, 12, 10, 18, 13, 17, 15, 13, 12, 12]
studiepunten = [6, 11, 5, 3, 3, 7, 4, 7, 7, 3, 4]
np.average(scores, weights=studiepunten)
14.266666666666667

Meetkundig gemiddelde#

Het meetkundig gemiddelde is belangrijk wanneer je een gemiddelde stijging of daling wil berekenen wanneer je de stijgingen en dalingen in procent weet.

Een voorbeeldje maakt dit misschien duidelijk. Een aandeel verandert iedere dag van koers. De verandering wordt in procenten opgegeven: de eerste dag stijgt het aandeel met 5 procent, de tweede dag met 3 procent en de derde dag daalt het aandeel met 2 procent. De vraag is nu: met hoeveel procent is het aandeel iedere dag gemiddeld gestegen of gedaald?

We kunnen hiervoor niet het gemiddelde van 5, 3 en -2 nemen. De percentages geven namelijk een stijging of daling weer, ten opzichte van de vorige waarde. Als een waarde stijgt met 5 procent, doen we de waarde eigenlijk maal 1.05 en als een waarde daalt met 2 procent, dan doen we deze maal 0.98.

De waarde is dus, na drie dagen, vermenigvuldigd met 1.05, dan met 1.03 en dan met 0.98. In totaal is de waarde vermenigvuldigd met 1.0599. Om nu te weten hoeveel dat gemiddeld per dag is, moeten we hieruit de derdemachtswortel trekken. Dat levert 1.019571. Dit noemt men het meetkundig gemiddelde. De waarde is dus per dag gemiddeld met 1.9571 procent gestegen.

In Python kan je dit als volgt berekenen (we gaan hier niet dieper in op de wiskunde erachter):

koers = pd.Series([5, 3, -2])
koers = koers / 100 + 1
(np.exp(np.mean(np.log(koers))) - 1) * 100
1.9571138432914026

Tip

We gebruiken hier de functies np.exp(), np.mean() en np.log() omdat exp() en log() niet als methoden bestaan op een Series.

Er is ook een functie beschikbaar die de berekening in 1 keer doet:

from scipy import stats

(stats.gmean(koers) - 1) * 100
1.9571138432914026

Harmonisch gemiddelde#

Er is nog een andere situatie waarin het rekenkundig gemiddelde niet werkt. Stel dat een auto heen en terug rijdt naar een bepaalde bestemming. In het heengaan rijdt de auto 120 km/u en in het terugkeren rijdt de auto 100 km/u. De vraag is nu: hoe snel rijdt de auto gemiddeld?

Je zou kunnen denken dat het antwoord 110 km/u moet zijn, maar als je erover redeneert, zie je dat dit verkeerd is. Stel bijvoorbeeld dat de afstand 120 km was. In het heengaan, zal de auto er dus een uur over rijden. In het terugkeren doet de auto er langer over: 1u en 12 minuten, ofwel 1.2 uur.

De auto heeft dus in totaal 240 km afgelegd in 2.2 uur. Dat is gemiddeld 109.09 km/u!

Je kan deze waarde vinden door het harmonisch gemiddelde te berekenen. In Python vind je dit zo:

snelheid = pd.Series([120, 100])
1 / np.mean(1 / snelheid)
109.0909090909091

Of met de hmean functie uit stats:

stats.hmean(snelheid)
109.0909090909091

Intuïtief begrijpen

Wil je beter begrijpen wat een rekenkundig en een meetkundig gemiddelde eigenlijk betekenen?

  • het rekenkundig gemiddelde berekent juist dàt getal waardoor je alle originele getallen uit de reeks kan vervangen en waarmee je toch dezelfde som zal bekomen.

Voorbeeld

1,2,3,4,5, som is 15, rekenkundig gemiddelde is 3, want 3+3+3+3+3=15

  • het meetkundig gemiddelde berekent juist dàt getal waardoor je alle originele getallen uit de reeks kan vervangen en waarmee je toch hetzelfde product zal bekomen.

Voorbeeld

1,2,3,4,5, product is 120, meetkundig gemiddelde is 2.605, want 2.6052.6052.6052.6052.605=120

Voortschrijdend gemiddelde#

Wanneer data niet uit een bestand komt, maar uit een stream (een constante aanvoer van data), dan wordt het soms zinloos om het gemiddelde te berekenen van alle getallen. Meestal is men dan enkel geïnteresseerd in het gemiddelde van de laatste n metingen. Als je dat doet, spreekt met van een voortschrijdend gemiddelde. Het wordt veel gebruikt om voorspellingen te doen. Daarom dat dit meer uitvoerig wordt besproken in het deeltje over forecasting.