Lese og rydde kjemiske data#

Læringsutbytte

Etter å ha arbeidet med dette temaet, skal du kunne:

  1. forklare hvordan data er organisert i en CSV-fil

  2. lese datafiler med Pandas

  3. undersøke kolonner, datatyper og manglende verdier

  4. filtrere, sortere og gruppere kjemiske data

  5. opprette nye kolonner uten å overskrive rådataene

  6. dokumentere og begrunne valg som tas under datarydding

Data er overalt, men data er ikke automatisk kunnskap. Før vi kan trekke en kjemisk konklusjon, må vi vite hva målingene representerer, hvilke enheter som er brukt, og om datasettet inneholder manglende eller ugyldige verdier.

I dette kapitlet følger vi et UV–Vis-forsøk. Datasettet inneholder blankprøver, kalibreringsstandarder og replikatmålinger av en ukjent prøve. Seinere skal vi bruke standardene til å lage en kalibreringsmodell og bestemme konsentrasjonen i den ukjente prøven. Først må vi lese og undersøke rådataene.

Datafiler#

Vi ønsker ofte å oppbevare og overføre data som råtekst fordi formatet er robust og kan leses av mange programmer. En Word-fil er ikke råtekst, fordi den også inneholder informasjon om blant annet skrifttyper, farger og formatering. .txt- og .csv-filer er vanlige råtekstformater.

Datafil

En datafil lagrer observasjoner i en fast struktur slik at de kan leses og behandles av et program. I en tabulær datafil er hver rad vanligvis én observasjon, mens kolonnene beskriver ulike variabler.

CSV står for comma-separated values. Hver linje representerer vanligvis én observasjon, mens verdiene på linja er skilt med komma:

sample_id,sample_type,concentration_uM,replicate,absorbance
blank_1,blank,0,1,0.010
blank_2,blank,0,2,0.011
std_2_1,standard,2,1,0.171
std_2_2,standard,2,2,0.169

Den første linja inneholder kolonnenavn. Hver av de neste linjene beskriver én måling. Dette kalles ofte et langt eller ryddig dataformat: Hver rad er en observasjon, hver kolonne er en variabel og hver celle inneholder én verdi.

Enheter i data

Enhetene bør være tydelige, men selve måleverdiene bør lagres som tall. Her er enheten lagt inn i kolonnenavnet concentration_uM. Et annet alternativ er å lagre enheter i egne metadata. Verdier som "2 µmol/L" blir tekst og er vanskeligere å regne med.

Lese data med Pandas#

Pandas er et mye brukt bibliotek for behandling av tabulære data. Når vi leser ei fil med read_csv, får vi en dataramme (DataFrame).

Dataramme

En dataramme er en todimensjonal datastruktur med navngitte kolonner og rader. Den kan sammenliknes med en tabell, men kolonnene kan behandles direkte med kode.

import pandas as pd

raw_data = pd.read_csv("data/uvvis_raw.csv")
print(raw_data)

Filbanen data/uvvis_raw.csv betyr at fila ligger i mappa data, som igjen ligger i samme mappe som notebooken eller programmet. Dersom fila ligger et annet sted, må filbanen endres.

Noen filer bruker semikolon som skilletegn og komma som desimalskilletegn. Da kan vi spesifisere dette:

data = pd.read_csv("data/example_semicolon.csv", sep=";", decimal=",")

Det er bedre å beskrive formatet eksplisitt enn å åpne fila manuelt og erstatte tegn uten å dokumentere det.

Undersøke datasettet#

Før vi analyserer dataene, bør vi undersøke hva vi faktisk har lest:

import pandas as pd

raw_data = pd.read_csv("data/uvvis_raw.csv")

print(raw_data.head())
print(raw_data.tail())
print(raw_data.shape)
print(raw_data.columns)
print(raw_data.dtypes)
  • head() viser de fem første radene.

  • tail() viser de fem siste.

  • shape gir antall rader og kolonner.

  • columns viser kolonnenavnene.

  • dtypes viser hvilken datatype Pandas har gitt hver kolonne.

Metoden info() gir en samlet oversikt:

raw_data.info()

Tekstkolonner får ofte datatypen object eller string, heltall får int64, og desimaltall får float64. En kolonne som burde være numerisk, men som har datatypen object, kan inneholde tekst, feil desimalskilletegn eller symboler som ikke kan tolkes som tall.

Du kan undersøke et lite UV–Vis-datasett i editoren nedenfor. Datasettet er lagt direkte inn i programmet slik at eksempelet fungerer i nettleseren.

Underveisoppgave

  1. Hvor mange rader og kolonner har datasettet?

  2. Hvilke kolonner inneholder tekst, og hvilke inneholder tall?

  3. Hvorfor er det manglende verdier i concentration_uM for den ukjente prøven?

  4. Hvor finnes den andre manglende verdien?

Bevar rådataene#

Rådata bør normalt ikke overskrives. Dersom vi endrer verdier direkte i raw_data, kan det bli vanskelig å rekonstruere hva instrumentet faktisk registrerte. Vi lager derfor en kopi som kan bearbeides:

clean_data = raw_data.copy()

Dette gir en enkel arbeidsflyt:

  1. raw_data inneholder dataene slik de ble lest.

  2. clean_data inneholder dokumenterte endringer.

  3. Analysen utføres på den bearbeidede kopien.

I et større prosjekt bør også selve rådatafila være skrivebeskyttet eller oppbevares separat. Koden fungerer da som en sporbar beskrivelse av hvordan rådata ble behandlet.

Manglende verdier#

Pandas representerer ofte en manglende verdi som NaN, som står for not a number. Vi kan telle manglende verdier slik:

print(raw_data.isna().sum())

Vi kan vise alle radene som mangler absorbans:

missing_absorbance = raw_data[raw_data["absorbance"].isna()]
print(missing_absorbance)

Det finnes ingen automatisk riktig måte å behandle en manglende verdi på. Mulige valg er blant annet å:

  • undersøke instrumentfila eller laboratorienotatene

  • utføre målingen på nytt

  • beholde raden og markere at verdien mangler

  • utelate raden fra en bestemt analyse

  • estimere verdien dersom det finnes en faglig og statistisk begrunnelse

I dette caset står det i instrumentloggen at målingen std_6_3 mislyktes fordi kyvetten ikke sto riktig. Vi kan derfor utelate akkurat denne raden fra analyser som krever absorbans:

clean_data = raw_data.copy()
clean_data = clean_data.dropna(subset=["absorbance"])

Vi bruker subset for å spesifisere hvilken kolonne som er relevant. Dersom vi hadde brukt dropna() uten argumenter, ville også alle radene for den ukjente prøven blitt fjernet fordi konsentrasjonen deres med hensikt er ukjent.

Viktig

En manglende verdi og en målt verdi lik null er ikke det samme. Null kan være et gyldig resultat. NaN betyr at verdien ikke er tilgjengelig.

Duplikater#

En rad kan ved en feil ha blitt lagret to ganger. Vi kan undersøke dette med:

duplicates = raw_data.duplicated()
print(raw_data[duplicates])

Fullstendig identiske rader kan fjernes med drop_duplicates, men også dette må begrunnes. To like tallverdier kan være to reelle replikater. De er bare duplikater dersom hele raden representerer den samme registreringen, for eksempel samme sample_id.

duplicate_ids = raw_data.duplicated(subset=["sample_id"], keep=False)
print(raw_data[duplicate_ids])

Filtrere data#

Vi kan velge ut rader ved hjelp av logiske vilkår. Her henter vi bare kalibreringsstandardene:

standards = clean_data[clean_data["sample_type"] == "standard"]
print(standards)

Den logiske testen lager først en serie med True og False. Når serien brukes mellom klammeparentesene, beholdes bare radene der resultatet er True.

Vi kan kombinere flere vilkår. Hvert vilkår må stå i parentes:

selected = clean_data[
    (clean_data["sample_type"] == "standard")
    & (clean_data["concentration_uM"] >= 4)
]

Andre nyttige operatorer er | for «eller», != for «ikke lik» og ~ for å negere et boolsk uttrykk.

Underveisoppgave

Skriv kode som velger ut:

  1. alle blankprøvene

  2. alle standardene med konsentrasjon mindre enn eller lik 6 µmol/L

  3. alle rader som ikke tilhører den ukjente prøven


Sortere data#

Vi kan sortere etter én eller flere kolonner:

sorted_data = clean_data.sort_values(
    by=["sample_type", "concentration_uM", "replicate"]
)

Sortering endrer ikke den kjemiske betydningen av dataene, men kan gjøre det lettere å oppdage uventede verdier eller kontrollere at replikater hører til riktig prøve.

Gruppere og oppsummere data#

Kalibreringsstandardene er målt i replikater. Vi kan gruppere dem etter konsentrasjon og beregne antall målinger, gjennomsnitt og standardavvik:

standards = clean_data[clean_data["sample_type"] == "standard"]

summary = standards.groupby("concentration_uM")["absorbance"].agg(
    ["count", "mean", "std"]
)

print(summary)

groupby deler først radene i grupper med samme konsentrasjon. agg bestemmer hvilke statistiske størrelser som skal beregnes for hver gruppe.

Du kan rydde og gruppere datasettet i editoren nedenfor.

Underveisoppgave

  1. Legg til min og max i oppsummeringen.

  2. Hvorfor mangler standardavviket dersom en gruppe bare har én måling?

  3. Sammenlikn spredningen ved de ulike konsentrasjonene. Er det grunnlag for å si at spredningen øker med absorbansen?

Lage nye kolonner#

Noen behandlingstrinn gir nye verdier som bør lagres uten å slette originalmålingen. Vi kan for eksempel korrigere absorbansen for middelverdien til blankprøvene:

blank_mean = clean_data.loc[
    clean_data["sample_type"] == "blank",
    "absorbance",
].mean()

clean_data["blank_corrected_absorbance"] = (
    clean_data["absorbance"] - blank_mean
)

Den opprinnelige kolonnen absorbance beholdes, mens den nye kolonnen dokumenterer den beregnede verdien. Dette gjør det mulig å kontrollere beregningen og sammenlikne rå og korrigerte data.

loc brukes her til å velge bestemte rader og én bestemt kolonne. Før komma står radvilkåret; etter komma står kolonnenavnet.

Modellvalg

Blankkorreksjon er ikke bare en teknisk Pandas-operasjon. Når vi trekker fra blankverdien, antar vi at blankbidraget kan adderes til prøvens signal, og at det er representativt for målingene som korrigeres.

Rydde kolonnenavn og tekst#

Virkelige datafiler kan ha mellomrom, store bokstaver eller inkonsistent tekst. Vi kan standardisere kolonnenavn ved å fjerne alle blanke linjer i front av teksten med strip, gjøre alle bokstaver små med lower og bruke replace tik å erstatte alle mellomrom med understrek:

renamed_data = clean_data.copy()
renamed_data.columns = (
    renamed_data.columns
    .str.strip()
    .str.lower()
    .str.replace(" ", "_")
)

print(renamed_data.columns)

Her lager vi en egen kopi slik at vi kan sammenlikne de nye kolonnenavnene med originalen. I en faktisk analyse kan vi arbeide videre med renamed_data dersom vi ønsker de standardiserte navnene.

Tekstverdier kan behandles på samme måte:

clean_data["sample_type"] = (
    clean_data["sample_type"]
    .str.strip()
    .str.lower()
)

Vi bør alltid kontrollere resultatet etter en slik operasjon. Automatisk rydding kan skjule at to kategorier faktisk betyr forskjellige ting.

Fra dataramme til figur#

Pandas-kolonner kan sendes direkte til Matplotlib:

import matplotlib.pyplot as plt

standards = clean_data[clean_data["sample_type"] == "standard"]

plt.scatter(standards["concentration_uM"], standards["absorbance"])
plt.xlabel("Concentration (µmol/L)")
plt.ylabel("Absorbance")
plt.title("UV–Vis calibration standards")
plt.tight_layout()
plt.show()

Her vises alle replikatene. Det gjør spredningen synlig. Dersom vi bare plotter gjennomsnittene, blir figuren ryddigere, men vi mister informasjon om variasjonen mellom målingene.

En reproduserbar arbeidsflyt#

En enkel og ryddig analyse kan bygges opp slik:

import pandas as pd

# 1. Read and preserve raw data
raw_data = pd.read_csv("data/uvvis_raw.csv")

# 2. Inspect
print(raw_data.info())
print(raw_data.isna().sum())

# 3. Create a working copy
clean_data = raw_data.copy()

# 4. Apply a justified cleaning decision
clean_data = clean_data.dropna(subset=["absorbance"])

# 5. Create derived values
blank_mean = clean_data.loc[
    clean_data["sample_type"] == "blank",
    "absorbance",
].mean()
clean_data["blank_corrected_absorbance"] = (
    clean_data["absorbance"] - blank_mean
)

# 6. Save processed data without overwriting the raw file
clean_data.to_csv("data/uvvis_processed.csv", index=False)

Kodekommentarene dokumenterer hva som er gjort, men en fullstendig analyse må også forklare hvorfor valgene er rimelige.

Oppgaver#

Oppgave 2.1

Åpne uvvis_raw.csv som råtekst. Identifiser kolonnenavn, skilletegn, manglende verdier og hvilke kolonner som inneholder enheter.

Oppgave 2.2

Les uvvis_raw.csv med Pandas. Bruk head, shape, columns, dtypes og info til å lage en kort beskrivelse av datasettet.

Oppgave 2.3

Forklar hvorfor det vil være feil å bruke dropna() uten subset på hele UV–Vis-datasettet. Demonstrer hva som skjer, men start på nytt fra rådata etterpå.

Oppgave 2.4

Lag et lite datasett med én fullstendig duplikatrad og to reelle replikater som tilfeldigvis har samme absorbans. Bruk duplicated til å vise hvorfor vi må vurdere mer enn bare måleverdien.

Oppgave 2.5

Velg ut blankprøvene og beregn middelverdien. Opprett en ny kolonne med blankkorrigert absorbans. Kontroller at middelverdien til de blankkorrigerte blankprøvene er omtrent null.

Oppgave 2.6

Grupper standardene etter konsentrasjon og beregn count, mean, std, min og max. Hvilke av størrelsene er vanskeligst å tolke når en gruppe bare har to målinger?

Oppgave 2.7

Fila reaction_kinetics.csv inneholder tid og absorbans. Les fila, undersøk datatypene, sorter etter tid og lag en ny kolonne relative_absorbance der hver absorbans deles på startverdien.

Oppgave 2.8

En fil inneholder kolonnene Temperature, temperature og TEMP. Skriv en kort vurdering av hvorfor automatisk standardisering ikke nødvendigvis er nok. Hvordan kan du finne ut om kolonnene representerer samme størrelse og enhet?

Oppgave 2.9

Lag en figur av alle kalibreringsstandardene fra den ryddede datarammen. Bruk forskjellig markør for hver replikatserie. Vurder om dette gjør figuren mer eller mindre informativ.

Oppgave 2.10 – samlet case

Lag et fullstendig program som leser uvvis_raw.csv, undersøker manglende verdier, lager en bearbeidet kopi, fjerner den dokumenterte mislykkede absorbansmålingen, blankkorrigerer dataene og lagrer resultatet som en ny CSV-fil. Programmet skal aldri endre rådatafila.

Videoer#

I videoene nedenfor kan du få en innføring eller repetisjon i filinnlesing og databehandling med Pandas.