Dataanalyse

Etter en rask intrukduksjon til numpy og pandas, kan man begynne på sitt første dataanalyse-prosjekt.

Den beste definisjonen av dataanalayse vil være å prøve å besvare spørsmål ved å bruke data. I bunn og grunn er det nettopp dette som er poenget. Dette er likevel en forenklet sannhet da det er vanskelig å gi en generell definisjon for et så bredt fagfelt.

Her er noen eksempler der dataanalyse benyttes til å besvare spørsmål, store og små:

Vår data

Nå skal vi jobbe med en dataframe om hauker. Dataene ble samlet under fuglefangsten av studenter ved Cornell College, USA. Datasettet inkluderer fugleartene: Rødhalevåk, Tverrhalehauk og Trostehauk:

hawks from a dataframe

Image credit: wikipedia and allaboutbirds.org

import pandas as pd
import numpy as np
hawks = pd.read_csv('https://raw.githubusercontent.com/vincentarelbundock/Rdatasets/refs/heads/master/csv/Stat2Data/Hawks.csv', index_col=0)
hawks.head()

Første steg i en skikkelig dataanalyse er å sette et mål, eller et spørsmål som skal besvares. Dette bestemmes normalt av prosjektet man jobber med. Her ønsker vi å besvare dette spørsmålet:

Hvordan skilles de ulike haukearter basert på fysiske egenskaper?

For å analysere dette spørsmålet effektivt, må det brytes ned til mindre, håndterbare steg. Det vil si:

Man kan dele variabene i ulike kategorier:

Det er viktig å forstå hva dataene man skal analysere faktisk handler om. En beskrivelse av variablene kan hjelpe oss med dette (kilde)

Kolonne Beskrivelse

Month: 8 september til 12 desember

Day: Dag i måneden

Year: År: 1992-2003

CaptureTime: Tid fanget (HH:MM)

ReleaseTime: Tid løslagg (HH:MM)

BandNumber: ID-kode bånd

Species: CH=Cooper’s, RT=Red-tailed, SS=Sharp-shinned

Age: A=Adult or I=Immature

Sex: F=Female or M=Male

Wing: Lengde (i mm) av hovedvingefjær fra ende til ledd

Weight: Kroppsvekt (i gram)

Culmen: Lengde (i mm) av overnebbet der fra tuppen til der det treffer ansiktet

Hallux: Lengde (i mm) av rovkloen

Tail: Mål (i mm) i forindelse med halens lengde (oppfunnet ved MacBride rovfuglsenter)

StandardTail: Standardmål av halelengden (i mm)

Tarsus: Lengde av fotbeinet (i mm)

WingPitFat: Fettmengde i vingespissen

KeelFat: Fettmengde på brystbeinet (measured by feel)

Crop: Kroens fyllingsgrad, justert fra 1=full til 0=tom

Dataoversikt

Første steg når man jobber med et datasett er å få et godt overbill over dataenes struktur og innhold:

hawks.info()
# <class 'pandas.core.frame.DataFrame'>
# Index: 908 entries, 1 to 908
# Data columns (total 19 columns):
#  #   Column        Non-Null Count  Dtype  
# ---  ------        --------------  -----  
#  0   Month         908 non-null    int64  
#  1   Day           908 non-null    int64  
#  2   Year          908 non-null    int64  
#  3   CaptureTime   908 non-null    object 
#  4   ReleaseTime   907 non-null    object 
#  5   BandNumber    908 non-null    object 
#  6   Species       908 non-null    object 
#  7   Age           908 non-null    object 
#  8   Sex           332 non-null    object 
#  9   Wing          907 non-null    float64
#  10  Weight        898 non-null    float64
#  11  Culmen        901 non-null    float64
#  12  Hallux        902 non-null    float64
#  13  Tail          908 non-null    int64  
#  14  StandardTail  571 non-null    float64
#  15  Tarsus        75 non-null     float64
#  16  WingPitFat    77 non-null     float64
#  17  KeelFat       567 non-null    float64
#  18  Crop          565 non-null    float64
# dtypes: float64(9), int64(4), object(6)
# memory usage: 141.9+ KB

Det er viktig å undersøke oppsummerende statistikk (som gjennomsnitt, median og spredning) gruppert etter art for å oppdage viktige forskjeller. Dette kan hjelpe oss med å se mønstre, for eksempel hvis en art har en tendens til å være større eller tyngre enn en annen.

Vi starter med å telle antall observasjoner av hver art. NB: samme hauk kan ha blitt observert flere ganger.

# teller hvor mange ganger hver fugl ble observert i snitt (båndnummer identifiserer en fugl)
print((hawks['BandNumber'].value_counts().sort_values(ascending=False)), "\n")

# beholder kun distinkte båndnummer (fjerner duplikate fugler)
hawks_unique = hawks.drop_duplicates(subset=['BandNumber'], keep='last')  # beholder siste observasjon
# teller etter art 
print(hawks_unique['Species'].value_counts().sort_values())
# Species
# CH     69
# SS    261
# RT    577

La oss se på litt oppsummerende statistikk etter art - først for numeriske data, så for faktorer. For enkelhets skyld konverteres noen av kolonnene til annen enhet.

hawks['Weight_kg'] = hawks['Weight'] / 1000 # konverterer vekt til kg
hawks['Culmen_cm'] = hawks['Culmen'] / 10   # konverteres til cm
hawks['Hallux_cm'] = hawks['Hallux'] / 10   # konverteres til cm
hawks['Wing_cm'] = hawks['Wing'] / 10       # konverteres til cm

print(hawks[['Wing_cm', 'Weight_kg', 'Culmen_cm', 'Hallux_cm', 'Species']].groupby('Species').mean().round(2))
#          Wing_cm  Weight_kg  Culmen_cm  Hallux_cm
# Species                                          
# CH         24.41       0.42       1.76       2.28
# RT         38.33       1.09       2.70       3.20
# SS         18.49       0.15       1.15       1.50

For å slippe å regne alt manuelt, kan man bruke describe()-metoden:

print(hawks.describe())

Oppsummerende statistikk er nyttig, men det forteller ikke alltid hele historien:

Men hva med kategoriske variabler?

# printer kolonner med dtype objekt
print(hawks.select_dtypes(include='object').sample(10))

Hvilke konklusjoner kan man gjøre fra dette?

Rensing av data

Det er særlig to ting å legge merke til fra disse resultatene:

  1. Dataene ble konvertert til riktig datatype, som gjør analyseprosessen litt enklere.

  2. Det er 908 observasjoner totalt, men noen av variablene har manglende verdier (NaN, inneholder ingen verdier)

Punkt nr 2 kan bli et problem. Generelt ekslkuderer operasjoner manglende data, men dette kan gi uønskede resultater:

try:
    hawks['Weight_int'] = hawks['Weight'].astype('int')
except ValueError as e:
    print("IntCastingNaNError:", e)
# IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer

Vi må altså behandle disse manglende verdiene. Her har vi flere muligheter:

  1. La de være…
  2. fjerne manglende verdier - fjern hele rader/ kolonner. Dette fungerer fint når mange verdier mangler for en bestemt trekk eller observasjon. Hvis ikke mister man for mye data.
  3. Fylle med satt verdi, ofte gjennomsnitt, median eller noe annet som gir mening. Ok hvis variasjonen i dataene ikke er veldig stor og hvis det kun gjelder få manglende verdier.
  4. Interpolasjon - fyller inn med en gjettet verdi basert på tilgjengelige data
  5. Predikere manglende verdier (med for eksempel lineær regresjon)
columns_to_fill = ['Weight', 'Wing', 'Tail']                    # viktig data med lite NaN
columns_to_drop = ['Tarsus', 'WingPitFat']                      # ikke så viktig data med mye NaN
columns_to_interpolate = ['StandardTail', 'KeelFat', 'Crop']    # en mellomting
# 1. fjerne verdier
hawks = hawks.drop(columns=columns_to_drop)    # fjerner kolonnene fra analysen
# eller  
bad_rows = hawks[hawks.isna().sum(axis=1) > 3].index    # identifiserer observasjoner med mer enn 3 manglende verdier
hawks = hawks.drop(index=bad_rows)                      # fjerner disse observasjonene

# 3. interpolasjon 
for column in columns_to_interpolate:
    hawks[column] = hawks[column].interpolate(method='linear')  # interpolering av manglende verdier
# 2. fylling med faste verdier
for column in columns_to_fill:  
    columns_mean = hawks[column].mean()                 # calculate mean of the columns
    hawks[column] = hawks[column].fillna(columns_mean)  # fill missing values with mean

# mer korrekt enn å fylle inn med gjennomsnitt kunne vært
# å finne gjennomsnitt for hver art separat - spesifikt gjennomsnitt
hawks = pd.read_csv('https://raw.githubusercontent.com/vincentarelbundock/Rdatasets/refs/heads/master/csv/Stat2Data/Hawks.csv', index_col=0)

for column in columns_to_fill:
    species_means = hawks.groupby('Species')[column].transform('mean')  # kalkulerer art-spesifikke gjennomsnitt
    hawks[column] = hawks[column].fillna(species_means)  # fyller manglende verdier med de arts-spesifikke gjennomsnittene

Korrelasjon

Korrelasjon måler både styrke og retning til den lineære sammenhengen mellom to numeriske variabler. I sin enkleste form, refererer det til sammenhengen mellom to variablene. Korrelasjon blir oppgitt som et tall mellom -1 og 1. Her er noen eksempler:

La oss beregne korrelasjoner mellom de numeriske variablene i dataframen:

hawks.select_dtypes(include='float').corr().round(2)

Her er det viktig å huske at korrelasjon ikke betyr kausal sammenheng.