Jdi na navigaci předmětu

09: Zpracování a analýza dat

Tento notebook je výukovým materiálem v předmětu BI-JUL.21 vyučovaném v zimním semestru akademického roku 2023/2024 Tomášem Kalvodou. Tvorba těchto materiálů byla podpořena NVS FIT.

Hlavní stránkou předmětu, kde jsou i další notebooky a zajímavé informace, je jeho Course Pages stránka.

versioninfo()
Julia Version 1.9.3
Commit bed2cd540a1 (2023-08-24 14:43 UTC)
Build Info:
  Official https://julialang.org/ release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 8 × Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-14.0.6 (ORCJIT, skylake)
  Threads: 2 on 8 virtual cores

1. DataFrames.jl

Pro práci s daty jistě znáte Pythonovský nástroj pandas. DataFrames.jl je v podstatě Julia analog tohoto nástroje. Pokud jste zvyklí pandas používat, může pro vás být užitečné porovnání pandas s Dataframes.jl.

using DataFrames

1.1 Jak vytvořit DataFrame?

DataFrame lze vytvořit mnoha způsoby. Můžeme začít s prázdnou tabulkou a postupně ji naplnit daty, nebo využít existující matici, nebo data načíst z externího souboru.

Prázdný DataFrame vytvoříme velmi snadno:

DataFrame()

0 rows × 0 columns


Keyword argumenty a NamedTuple

Data v pojmenovaných sloupcích můžeme předat pomocí keyword argumentů (klíčové slovo je název sloupce, hodnota data):

DataFrame(course=["BI-LA1", "BI-DML", "BI-MA1", "BI-MA2"], semester=[1, 1, 2, 3], department=18105)

4 rows × 3 columns

coursesemesterdepartment
StringInt64Int64
1BI-LA1118105
2BI-DML118105
3BI-MA1218105
4BI-MA2318105

Tímto způsobem bychom ovšem měli problém zadat data se sloupcích, jejichž názvy obsahují třeba speciální znaky jako mezery. K tomu můžeme použít slovník, resp. dvojice (klíče mohou být řetězce nebo symboly -- ty jsou doporučené, místo mezer je vhodnější použít podtžítka :slovo_slovo):

df = DataFrame("fiktivní postava" => ["Gandalf", "Harry Potter"], "kniha" => ["Pán prstenů", "Harry Potter a kámen mudrců"])

2 rows × 2 columns

fiktivní postavakniha
StringString
1GandalfPán prstenů
2Harry PotterHarry Potter a kámen mudrců

Další možností je použít NamedTuple:

nt = (a = 1, b = 3)
(a = 1, b = 3)
nt[1], nt[2]
(1, 3)
nt[:a], nt[:b]
(1, 3)

Po sloupcích (pozor na jemný rozdíl od výše uvedeného způsobu):

DataFrame((a = [1,2,3], b = [4,5,6]))

3 rows × 2 columns

ab
Int64Int64
114
225
336

Po řádcích:

DataFrame([(a = 1, b = 4), (a = 2, b = 5), (a = 3, b = 6)])

3 rows × 2 columns

ab
Int64Int64
114
225
336
DataFrame([(a = 1, b = 4, c = nothing), (a = 2, b = 5, c = nothing), (a = 3, b = nothing, c = 6)])

3 rows × 3 columns

abc
Int64Union…Union…
114
225
336

Matice

K vytvoření DataFrame můžeme použít i matici, jen musíme vyřešit pojmenování sloupců. Automaticky (nutno zadat jako druhý argument :auto) budou označeny jako x1, x2, atd.:

DataFrame(rand(2, 3), :auto)

2 rows × 3 columns

x1x2x3
Float64Float64Float64
10.2375910.09401760.250011
20.3151520.02001350.819533
DataFrame(rand(Int64, 2, 3), :auto)

2 rows × 3 columns

x1x2x3
Int64Int64Int64
12884220508743408539558676208905145960-5079403418050376869
2459619389581005574211952100903843613721829307102220557273

V druhém argumentu případně můžeme zadat naše názvy sloupců:

DataFrame(rand(2, 3), ["col1", "col2", "col3"])

2 rows × 3 columns

col1col2col3
Float64Float64Float64
10.3380290.2131820.360838
20.6857780.305920.674301

CSV.jl

V tomto notebooku budeme pro některé ukázky používat CSV anonymizovaný export z Grades předmětu BI-MA1 v semestru B212 (2021/2022). Importu dat z CSV lze snadno docílit pomocí balíčku CSV.jl, který přidáme standardně ] add CSV a pak importujeme:

using CSV

Nyní stačí použít metodu read z modulu CSV, v prvním argumentu zadáme cestu k souboru a v druhém uvedeme požadovaný výstupní "formát", v našem případě DataFrame:

df = CSV.read(joinpath("files", "bi-ma1-b212.csv"), DataFrame)

528 rows × 16 columns (omitted printing of 9 columns)

usernametest1test2second_chanceactivitytests_totalgitlab
String15Float64?Float64?Float64?Float64?Float64Float64?
1student0012.0missingmissingmissing2.0missing
2student0029.016.0missingmissing25.0missing
3student0036.012.0missing5.018.0missing
4student0041.0missingmissing0.01.0missing
5student0054.0missingmissingmissing4.0missing
6student0062.0missingmissingmissing2.0missing
7student0079.56.0missingmissing15.5missing
8student0088.014.015.0missing25.0missing
9student00912.013.0missing4.025.0missing
10student0100.0missingmissingmissing0.0missing
11student0119.014.015.0missing25.0missing
12student012missingmissingmissingmissing0.0missing
13student01317.518.5missing2.036.0missing
14student0147.514.015.02.025.0missing
15student01515.010.0missing5.025.00.5
16student01612.015.0missing3.027.0missing
17student0176.016.522.03.025.0missing
18student0189.517.5missing0.527.0missing
19student0196.513.521.02.025.0missing
20student02011.59.510.5missing21.0missing
21student02110.013.519.04.025.0missing
22student022missingmissingmissingmissing0.0missing
23student0232.08.5missingmissing10.5missing
24student02410.015.0missingmissing25.0missing
25student02520.018.0missing4.538.07.0
26student0268.013.015.03.025.01.0
27student02715.018.5missing3.533.5missing
28student0284.00.0missing2.04.0missing
29student02913.012.5missing2.525.5missing
30student03013.512.0missing2.025.5missing

K těmto datům se vrátíme podrobněji později. Samotnou tabulku občas použijeme v různých ukázkách.


1.2 Práce s DataFrame

Z částečného výpisu dat výše vyvstává hned několik otázek:

  • Jaké sloupce jsou ještě k dispozici?
  • Proč je typ pod některými sloupci s otazníkem?
  • Co znamená missing?

Pojďme se vydat na průzkum. První, co můžeme zkusit, je podívat se na atributy naší instance (interaktivně pomocí TAB):

df.
df.username
528-element Vector{String15}:
 "student001"
 "student002"
 "student003"
 "student004"
 "student005"
 "student006"
 "student007"
 "student008"
 "student009"
 "student010"
 "student011"
 "student012"
 "student013"
 ⋮
 "student517"
 "student518"
 "student519"
 "student520"
 "student521"
 "student522"
 "student523"
 "student524"
 "student525"
 "student526"
 "student527"
 "student528"
df.assessment
528-element Vector{Bool}:
 0
 1
 0
 0
 0
 0
 0
 1
 1
 0
 1
 0
 1
 ⋮
 0
 1
 0
 0
 0
 0
 1
 1
 1
 1
 1
 1

Nebo k tomu můžeme použít metodu names z modulu DataFrames:

names(df)
16-element Vector{String}:
 "username"
 "test1"
 "test2"
 "second_chance"
 "activity"
 "tests_total"
 "gitlab"
 "assessment"
 "exam_test"
 "date"
 "oral_exam"
 "veto"
 "points_total"
 "mark"
 "tutor"
 "percentil"

Metoda eachcol vrátí iterátor přes sloupce. Můžeme tak zjistit typ prvků sloupců:

eltype.(eachcol(df))
16-element Vector{Type}:
 String15
 Union{Missing, Float64}
 Union{Missing, Float64}
 Union{Missing, Float64}
 Union{Missing, Float64}
 Float64
 Union{Missing, Float64}
 Bool
 Union{Missing, Int64}
 Union{Missing, String7}
 Union{Missing, Float64}
 Union{Missing, Bool}
 Union{Missing, Float64}
 Union{Missing, String1}
 Union{Missing, String7}
 Int64

Odtud vidíme, co znamenají otazníky u typů prvků sloupců. Prvky sloupce jsou složeného typu Union{Missing, T}, v některých sloupcích mohou hodnoty chybět, obsahují hodnotu missing.

Stručné informace můžeme získat pomocí metody describe. Samozřejmě občas uvedené statistiky nemají moc smysl.

describe(df)

16 rows × 7 columns

variablemeanminmedianmaxnmissingeltype
SymbolUnion…AnyUnion…AnyInt64Type
1usernamestudent001student5280String15
2test18.451990.08.020.0101Union{Missing, Float64}
3test213.31880.014.020.0219Union{Missing, Float64}
4second_chance15.05880.015.023.5460Union{Missing, Float64}
5activity1.956290.02.05.0242Union{Missing, Float64}
6tests_total14.9280.015.539.50Float64
7gitlab1.307690.01.07.0489Union{Missing, Float64}
8assessment0.39583300.010Bool
9exam_test16.7081017.020319Union{Missing, Int64}
10date---16:50319Union{Missing, String7}
11oral_exam48.302628.050.060.0338Union{Missing, Float64}
12veto1.011.01525Union{Missing, Bool}
13points_total79.621755.080.0103.5339Union{Missing, Float64}
14markAF319Union{Missing, String1}
15tutorHonzaTomáš8Union{Missing, String7}
16percentil53.11362350.01000Int64

Výpis můžeme kontrolovat uvedením konkrétního rozsahu.

describe(df, cols=["test1", "test2"])

2 rows × 7 columns

variablemeanminmedianmaxnmissingeltype
SymbolFloat64Float64Float64Float64Int64Union
1test18.451990.08.020.0101Union{Missing, Float64}
2test213.31880.014.020.0219Union{Missing, Float64}

DataFrame je tabulka, máme pro ní k dispozici podobné metody jako pro matice, na které se také lze dívat jako na "tabulky".

size(df)
(528, 16)
nrow(df)
528
ncol(df)
16

DataFrame lze kopírovat pomocí metody copy, nebo ho vyprázdnit pomocí metod empty, resp. empty!.

ex1 = DataFrame(rand(3, 2), :auto)
ex2 = copy(ex1)

3 rows × 2 columns

x1x2
Float64Float64
10.1274360.470857
20.1499810.35907
30.9682910.422546
empty!(ex2)
empty(ex1)

0 rows × 2 columns

x1x2
Float64Float64
ex1

3 rows × 2 columns

x1x2
Float64Float64
10.1274360.470857
20.1499810.35907
30.9682910.422546
ex2

0 rows × 2 columns

x1x2
Float64Float64

To by bylo pro začátek vše k základním vlastnostem. Pojďme nyní tabulky modifikovat, upravovat.


Indexování a přístup ke sloupcům

Indexování vychází z maticového zápisu a má podobné vlastnosti. Ke sloupcům můžeme přistupovat několika způsoby:

  • pomocí názvu a tečky: df.mark, df."mark"
  • pomocí indexace (nevytvoří kopii): df[!, :mark], df[!, "mark"]
  • pomocí indexace (vytvoří kopii): df[:, :mark], df[:, "mark"]

Tímto způsobem můžeme data číst, ale i je modifikovat.

df.mark
528-element PooledArrays.PooledVector{Union{Missing, String1}, UInt32, Vector{UInt32}}:
 missing
 "C"
 missing
 missing
 missing
 missing
 missing
 "C"
 "D"
 missing
 "C"
 missing
 "A"
 ⋮
 missing
 "D"
 missing
 missing
 missing
 missing
 "D"
 "E"
 "B"
 "F"
 "A"
 "D"
df.mark === df[!, :mark]
true
df.mark === df[:, :mark] # kopie!
false

Vedle toho nemusíme používat název sloupce, v pořádku je použít i pořadí. Například prvních deset řádků v druhém a třetím sloupci:

df[1:10, ["test2", "test1"]]

10 rows × 2 columns

test2test1
Float64?Float64?
1missing2.0
216.09.0
312.06.0
4missing1.0
5missing4.0
6missing2.0
76.09.5
814.08.0
913.012.0
10missing0.0

Not, Between, Cols, All

Dále můžeme vybírat pouze potřebné sloupce.

ex = DataFrame(rand(3, 5), :auto)

3 rows × 5 columns

x1x2x3x4x5
Float64Float64Float64Float64Float64
10.2648750.0915690.9256670.1458480.811026
20.6794590.4978660.8617290.5723220.883065
30.8239320.7790330.9727840.6843410.469005
ex[:, Not(:x2)]

3 rows × 4 columns

x1x3x4x5
Float64Float64Float64Float64
10.2648750.9256670.1458480.811026
20.6794590.8617290.5723220.883065
30.8239320.9727840.6843410.469005
Not(:x2)
InvertedIndex{Symbol}(:x2)
ex[:, Not([:x2, :x3])]

3 rows × 3 columns

x1x4x5
Float64Float64Float64
10.2648750.1458480.811026
20.6794590.5723220.883065
30.8239320.6843410.469005
ex[:, Between(:x1, :x3)]

3 rows × 3 columns

x1x2x3
Float64Float64Float64
10.2648750.0915690.925667
20.6794590.4978660.861729
30.8239320.7790330.972784
ex[:, Cols(:x2, :x4)]

3 rows × 2 columns

x2x4
Float64Float64
10.0915690.145848
20.4978660.572322
30.7790330.684341
ex[:, All()]

3 rows × 5 columns

x1x2x3x4x5
Float64Float64Float64Float64Float64
10.2648750.0915690.9256670.1458480.811026
20.6794590.4978660.8617290.5723220.883065
30.8239320.7790330.9727840.6843410.469005

Výše uvedené ukázky využívají funkcionalitu z DataFrames.jl balíčku. "Staré maticové" postupy samozřejmě fungují také, např.:

ex[:, [:x2, :x3]]

3 rows × 2 columns

x2x3
Float64Float64
10.0915690.925667
20.4978660.861729
30.7790330.972784

Přepisování dat

Údaje můžeme měnit několika způsoby. Vezměme si jednoduchou tabulku:

m = [ "α" 1 0.5; "β" 2 1.5; "γ" 3 2.5]
3×3 Matrix{Any}:
 "α"  1  0.5
 "β"  2  1.5
 "γ"  3  2.5
ex = DataFrame(m, ["a", "b", "c"])

3 rows × 3 columns

abc
AnyAnyAny
1α10.5
2β21.5
3γ32.5

Změna jedné položky (Ξ\Xi je velké ξ\xi):

ex[1, :a] = "Ξ"

ex

3 rows × 3 columns

abc
AnyAnyAny
1Ξ10.5
2β21.5
3γ32.5

Přepsání všech hodnot ve sloupci c:

ex[:, :c] = ["tau", "pi", "omega"]
3-element Vector{String}:
 "tau"
 "pi"
 "omega"
ex

3 rows × 3 columns

abc
AnyAnyAny
1Ξ1tau
2β2pi
3γ3omega

Přepsání jednou hodnotou.

ex[:, :c] .= "ř"
3-element view(::Vector{Any}, :) with eltype Any:
 "ř"
 "ř"
 "ř"
ex

3 rows × 3 columns

abc
AnyAnyAny
1Ξ1ř
2β2ř
3γ3ř
z = ex[:, :c]
3-element Vector{Any}:
 "ř"
 "ř"
 "ř"
z[2] = "pí"
"pí"
z
3-element Vector{Any}:
 "ř"
 "pí"
 "ř"
ex

3 rows × 3 columns

abc
AnyAnyAny
1Ξ1ř
2β2ř
3γ3ř
z = ex[!, :c]
3-element Vector{Any}:
 "ř"
 "ř"
 "ř"
z[2] = "¿"
"¿"
ex

3 rows × 3 columns

abc
AnyAnyAny
1Ξ1ř
2β2¿
3γ3ř
ex.c = [true, false, true]

ex

3 rows × 3 columns

abc
AnyAnyBool
1Ξ11
2β20
3γ31

Přepsání hodnot ve sloupci a danou hodnotou, ale pouze v řádcích, kde je hodnota ve sloupci b lichá (zde opět používáme indexování bitovým vektorem, nebo "maskování", s kterým jsme se setkali dříve během semestru):

ex[isodd.(ex.b), :a] .= "λ"

ex

3 rows × 3 columns

abc
AnyAnyBool
1λ11
2β20
3λ31

Občas je nutné projít tabulku řádek po řádku. V tom případě můžeme iterovat přes index. Například se pokusme změnit první sloupec na řetězec obsahující daný symbol se spodním indexem daným číslem ve sloupci b v LaTeX notaci (tj. λ_1 atd.).

for row_id = axes(ex, 1) # 1:nrow(df)
    ex[row_id, :a] = ex[row_id, :a] * "_" * string(ex[row_id, :b])
end

ex

3 rows × 3 columns

abc
AnyAnyBool
1λ_111
2β_220
3λ_331

Zobrazovaní (show, first, last, view)

Pokud je DataFrame příliš velký, tak při jeho zobrazení dojde k ořezání (řádků i sloupců). Toto chování můžeme přebít pomocí parametrů metody show.

U našeho BI-ZMA příkladu je to lehce overkill.

show(df)
528×16 DataFrame
 Row │ username    test1      test2      second_chance  activity   tests_total ⋯
     │ String15    Float64?   Float64?   Float64?       Float64?   Float64     ⋯
─────┼──────────────────────────────────────────────────────────────────────────
   1 │ student001        2.0  missing        missing    missing            2.0 ⋯
   2 │ student002        9.0       16.0      missing    missing           25.0
   3 │ student003        6.0       12.0      missing          5.0         18.0
   4 │ student004        1.0  missing        missing          0.0          1.0
   5 │ student005        4.0  missing        missing    missing            4.0 ⋯
   6 │ student006        2.0  missing        missing    missing            2.0
   7 │ student007        9.5        6.0      missing    missing           15.5
   8 │ student008        8.0       14.0           15.0  missing           25.0
   9 │ student009       12.0       13.0      missing          4.0         25.0 ⋯
  10 │ student010        0.0  missing        missing    missing            0.0
  11 │ student011        9.0       14.0           15.0  missing           25.0
  ⋮  │     ⋮           ⋮          ⋮            ⋮            ⋮           ⋮      ⋱
 519 │ student519        0.0  missing        missing    missing            0.0
 520 │ student520        1.0  missing        missing    missing            1.0 ⋯
 521 │ student521        7.0  missing        missing    missing            7.0
 522 │ student522  missing    missing        missing    missing            0.0
 523 │ student523        9.5       11.5           15.0        2.0         25.0
 524 │ student524        5.5       19.5      missing    missing           25.0 ⋯
 525 │ student525       14.5       16.0      missing          2.0         30.5
 526 │ student526       10.5       17.0      missing          2.0         27.5
 527 │ student527       12.5       19.5      missing          4.0         32.0
 528 │ student528       13.0       13.0      missing          1.0         26.0 ⋯
                                                 10 columns and 507 rows omitted
show(df, allcols=true)
528×16 DataFrame
 Row │ username    test1      test2      second_chance  activity   tests_total  gitlab     assessment  exam_test  date      oral_exam  veto     points_total  mark      tutor     percentil 
     │ String15    Float64?   Float64?   Float64?       Float64?   Float64      Float64?   Bool        Int64?     String7?  Float64?   Bool?    Float64?      String1?  String7?  Int64     
─────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ student001        2.0  missing        missing    missing            2.0  missing         false    missing  missing   missing    missing     missing    missing   Honza            29
   2 │ student002        9.0       16.0      missing    missing           25.0  missing          true         15  10:00          48.0  missing          73.0  C         Irena            44
   3 │ student003        6.0       12.0      missing          5.0         18.0  missing         false    missing  missing   missing    missing     missing    missing   Jitka            54
   4 │ student004        1.0  missing        missing          0.0          1.0  missing         false    missing  missing   missing    missing     missing    missing   Pavel            26
   5 │ student005        4.0  missing        missing    missing            4.0  missing         false    missing  missing   missing    missing     missing    missing   Irena            36
   6 │ student006        2.0  missing        missing    missing            2.0  missing         false    missing  missing   missing    missing     missing    missing   Jitka            29
   7 │ student007        9.5        6.0      missing    missing           15.5  missing         false    missing  missing   missing    missing     missing    missing   Honza            50
   8 │ student008        8.0       14.0           15.0  missing           25.0  missing          true         19  11:20          53.0  missing          78.0  C         Irena            59
   9 │ student009       12.0       13.0      missing          4.0         25.0  missing          true         16  10:00          33.0  missing          62.0  D         Jitka            71
  10 │ student010        0.0  missing        missing    missing            0.0  missing         false    missing  missing   missing    missing     missing    missing   Honza            23
  11 │ student011        9.0       14.0           15.0  missing           25.0  missing          true         19  10:20          53.0  missing          78.0  C         Jitka            71
  ⋮  │     ⋮           ⋮          ⋮            ⋮            ⋮           ⋮           ⋮          ⋮           ⋮         ⋮          ⋮         ⋮          ⋮           ⋮         ⋮          ⋮
 519 │ student519        0.0  missing        missing    missing            0.0  missing         false    missing  missing   missing    missing     missing    missing   Pavel            23
 520 │ student520        1.0  missing        missing    missing            1.0  missing         false    missing  missing   missing    missing     missing    missing   Honza            26
 521 │ student521        7.0  missing        missing    missing            7.0  missing         false    missing  missing   missing    missing     missing    missing   Jitka            42
 522 │ student522  missing    missing        missing    missing            0.0  missing         false    missing  missing   missing    missing     missing    missing   Irena            23
 523 │ student523        9.5       11.5           15.0        2.0         25.0  missing          true         17  10:15          40.0  missing          67.0  D         Pavel            59
 524 │ student524        5.5       19.5      missing    missing           25.0  missing          true         15  11:00          30.0  missing          55.0  E         Honza            71
 525 │ student525       14.5       16.0      missing          2.0         30.5  missing          true         18  10:00          53.0  missing          85.5  B         Pavel            86
 526 │ student526       10.5       17.0      missing          2.0         27.5  missing          true         13  ---       missing    missing     missing    F         Honza            80
 527 │ student527       12.5       19.5      missing          4.0         32.0  missing          true         18  10:00          56.0  missing          92.0  A         Pavel            90
 528 │ student528       13.0       13.0      missing          1.0         26.0        0.0        true         15  10:00          42.0  missing          69.0  D         Ivo              75
                                                                                                                                                                            507 rows omitted

Toto by byl ještě větší overkill.

#show(df, allrows=true)

Případně můžeme prozkoumávat začátek a konec tabulky.

first(df, 10)

10 rows × 16 columns (omitted printing of 9 columns)

usernametest1test2second_chanceactivitytests_totalgitlab
String15Float64?Float64?Float64?Float64?Float64Float64?
1student0012.0missingmissingmissing2.0missing
2student0029.016.0missingmissing25.0missing
3student0036.012.0missing5.018.0missing
4student0041.0missingmissing0.01.0missing
5student0054.0missingmissingmissing4.0missing
6student0062.0missingmissingmissing2.0missing
7student0079.56.0missingmissing15.5missing
8student0088.014.015.0missing25.0missing
9student00912.013.0missing4.025.0missing
10student0100.0missingmissingmissing0.0missing
last(df, 10)

10 rows × 16 columns (omitted printing of 9 columns)

usernametest1test2second_chanceactivitytests_totalgitlab
String15Float64?Float64?Float64?Float64?Float64Float64?
1student5190.0missingmissingmissing0.0missing
2student5201.0missingmissingmissing1.0missing
3student5217.0missingmissingmissing7.0missing
4student522missingmissingmissingmissing0.0missing
5student5239.511.515.02.025.0missing
6student5245.519.5missingmissing25.0missing
7student52514.516.0missing2.030.5missing
8student52610.517.0missing2.027.5missing
9student52712.519.5missing4.032.0missing
10student52813.013.0missing1.026.00.0

Pro rychlé prozkoumávání částí tabulky slouží metoda view, resp. makro @view, která jen část tabulky zobrazí. Nevytváří nový objekt, měla by být efektivnější.

view(df, 100:150, [:username, :points_total, :mark])

51 rows × 3 columns

usernamepoints_totalmark
String15Float64?String1?
1student100missingmissing
2student101missingmissing
3student10285.0B
4student103missingmissing
5student104missingmissing
6student10564.0D
7student106missingmissing
8student10790.0A
9student108missingmissing
10student10984.0B
11student110missingmissing
12student111missingmissing
13student112missingmissing
14student113missingF
15student114missingmissing
16student115missingmissing
17student11680.0B
18student117missingmissing
19student118missingF
20student119missingF
21student120missingmissing
22student121missingmissing
23student12280.0B
24student123missingmissing
25student124missingmissing
26student125missingmissing
27student126missingmissing
28student12780.0B
29student128missingmissing
30student129missingmissing
@view df[10:15, [:username, :mark]]

6 rows × 2 columns

usernamemark
String15String1?
1student010missing
2student011C
3student012missing
4student013A
5student014B
6student015B

Přidávání a odebírání sloupců

K přidávání sloupců slouží metoda insertcols!:

ex = DataFrame(rand(2, 2), :auto)

2 rows × 2 columns

x1x2
Float64Float64
10.1425160.965487
20.005055770.571855
insertcols!(ex, 1, :a => pi)

2 rows × 3 columns

ax1x2
Irration…Float64Float64
1π0.1425160.965487
2π0.005055770.571855
insertcols!(ex, 3, :b => "⊕")

2 rows × 4 columns

ax1bx2
Irration…Float64StringFloat64
1π0.1425160.965487
2π0.005055770.571855
insertcols!(ex, 5, :c => [1, 2])

2 rows × 5 columns

ax1bx2c
Irration…Float64StringFloat64Int64
1π0.1425160.9654871
2π0.005055770.5718552

Ale lze použít i prosté indexování:

ex[:, :d] = [42, 42]

ex

2 rows × 6 columns

ax1bx2cd
Irration…Float64StringFloat64Int64Int64
1π0.1425160.965487142
2π0.005055770.571855242

Mazání sloupců musíme provést pomocí indexace, neexistuje metoda "dropcolumns!".

ex[:, Not(:b)]

2 rows × 5 columns

ax1x2cd
Irration…Float64Float64Int64Int64
1π0.1425160.965487142
2π0.005055770.571855242

Přidávání a odebírání řádků

Vytvořme si zase testovací tabulku:

ex = DataFrame(rand(2, 2), :auto)

2 rows × 2 columns

x1x2
Float64Float64
10.9742320.598728
20.6550420.634699

Přidat řádek lze opět několika způsoby. Nejpřirozenější je asi metoda push!:

push!(ex, [1.0, 2.0])

3 rows × 2 columns

x1x2
Float64Float64
10.9742320.598728
20.6550420.634699
31.02.0
push!(ex, (x1 = 0.3, x2 = 0.5))

4 rows × 2 columns

x1x2
Float64Float64
10.9742320.598728
20.6550420.634699
31.02.0
40.30.5
push!(ex, (x2 = 7, x1 = 5))

6 rows × 2 columns

x1x2
Float64Float64
10.9742320.598728
20.6550420.634699
31.02.0
40.30.5
50.57.0
65.07.0

Pomocí append! můžeme spojovat několik tabulek "vertikálně" dohromady:

append!(ex, DataFrame(rand(10, 2), :auto))

16 rows × 2 columns

x1x2
Float64Float64
10.9742320.598728
20.6550420.634699
31.02.0
40.30.5
50.57.0
65.07.0
70.6429970.0414806
80.05832660.237347
90.937930.249314
100.7374530.495015
110.3376250.0557319
120.3446750.674127
130.09091820.0982035
140.124830.850773
150.2197310.093136
160.6290280.882152

Mazat řádky můžeme explicitně pomocí delete!:

delete!(ex, 3)

15 rows × 2 columns

x1x2
Float64Float64
10.9742320.598728
20.6550420.634699
30.30.5
40.57.0
55.07.0
60.6429970.0414806
70.05832660.237347
80.937930.249314
90.7374530.495015
100.3376250.0557319
110.3446750.674127
120.09091820.0982035
130.124830.850773
140.2197310.093136
150.6290280.882152
delete!(ex, [1, 2, 3])

12 rows × 2 columns

x1x2
Float64Float64
10.57.0
25.07.0
30.6429970.0414806
40.05832660.237347
50.937930.249314
60.7374530.495015
70.3376250.0557319
80.3446750.674127
90.09091820.0982035
100.124830.850773
110.2197310.093136
120.6290280.882152
ex.x1 .> 0.5
12-element BitVector:
 0
 1
 1
 0
 1
 1
 0
 0
 0
 0
 0
 1
delete!(ex, ex.x1 .> 0.5)

7 rows × 2 columns

x1x2
Float64Float64
10.57.0
20.05832660.237347
30.3376250.0557319
40.3446750.674127
50.09091820.0982035
60.124830.850773
70.2197310.093136

To zdaleka není všechno

Výše jsme shrnuli zřejmě ty nejužitečnější metody a techniky. Tím ale možnosti DataFrames.jl zdaleka nekončí. Zvídavému čtenáři doporučujeme prolétnout dokumentaci. Na tomto místě snad jen upozorněme na následující:

Velmi sofistikované transformace dat v tabulkách lze také provádět pomocí následujících metod:

  • combine
  • select/select!
  • transform/transform!

Jejich podrobný výklad je už nad rámec tohoto kurzu.


2. Cvičení: analýza BI-MA1 B212 (a B222)

Pojďme se podrobněji podívat na data z předmětu BI-MA1 v semestru B212, tedy první běh předmětu. Tehdejší pravidla pro získání zápočtu a složení zkoušky lze dohledat v gitlabu.

Nejprve znovu načteme data z CSV souboru, který je syrovým exportem z Grades, kde jsou anonymizováni uživatelská jména studentů.

using CSV
df1 = CSV.read(joinpath("files", "bi-ma1-b212.csv"), DataFrame)
df2 = CSV.read(joinpath("files", "bi-ma1-b222.csv"), DataFrame)

760 rows × 17 columns (omitted printing of 10 columns)

usernametest1test2test3second_chanceactivitytests_total
String15Float64?Float64?Float64?Float64?Float64?Float64?
1student00011.511.09.017.0missing25.0
2student00023.015.018.5missing2.036.5
3student0003missingmissingmissingmissingmissing0.0
4student0004missingmissingmissingmissingmissing0.0
5student0005missingmissingmissingmissingmissing0.0
6student0006missingmissingmissingmissingmissing0.0
7student00073.012.018.5missing1.533.5
8student00081.06.014.018.51.025.0
9student00092.016.520.0missing1.038.5
10student00101.010.513.5missingmissing25.0
11student00112.57.512.014.02.022.0
12student00120.514.510.5missingmissing25.5
13student00132.56.013.512.53.022.0
14student00143.010.5missingmissingmissing13.5
15student00150.00.0missingmissingmissing0.0
16student00163.06.0missingmissingmissing9.0
17student00170.01.0missingmissingmissing1.0
18student00181.015.013.0missingmissing29.0
19student0019missingmissingmissingmissingmissing0.0
20student00202.518.010.5missing1.031.0
21student0021missingmissingmissingmissingmissing0.0
22student00221.02.5missingmissingmissing3.5
23student00233.014.514.0missingmissing31.5
24student00242.516.07.5missing4.026.0
25student00251.0missingmissingmissingmissing1.0
26student00261.07.5missingmissingmissing8.5
27student00270.59.016.5missingmissing26.0
28student00283.013.518.5missing4.035.0
29student00293.05.512.514.0missing21.0
30student00300.5missingmissingmissing0.50.5
describe(df1)

16 rows × 7 columns

variablemeanminmedianmaxnmissingeltype
SymbolUnion…AnyUnion…AnyInt64Type
1usernamestudent001student5280String15
2test18.451990.08.020.0101Union{Missing, Float64}
3test213.31880.014.020.0219Union{Missing, Float64}
4second_chance15.05880.015.023.5460Union{Missing, Float64}
5activity1.956290.02.05.0242Union{Missing, Float64}
6tests_total14.9280.015.539.50Float64
7gitlab1.307690.01.07.0489Union{Missing, Float64}
8assessment0.39583300.010Bool
9exam_test16.7081017.020319Union{Missing, Int64}
10date---16:50319Union{Missing, String7}
11oral_exam48.302628.050.060.0338Union{Missing, Float64}
12veto1.011.01525Union{Missing, Bool}
13points_total79.621755.080.0103.5339Union{Missing, Float64}
14markAF319Union{Missing, String1}
15tutorHonzaTomáš8Union{Missing, String7}
16percentil53.11362350.01000Int64
size(df1)
(528, 16)
nrow(df1)
528
describe(df2)

17 rows × 7 columns

variablemeanminmedianmaxnmissingeltype
SymbolUnion…AnyUnion…AnyInt64Type
1usernamestudent0001student07600String15
2test11.698180.01.53.099Union{Missing, Float64}
3test210.66720.011.020.0132Union{Missing, Float64}
4test312.68930.013.520.0245Union{Missing, Float64}
5second_chance14.54025.015.022.0673Union{Missing, Float64}
6activity2.27454-6.02.05.0383Union{Missing, Float64}
7tests_total19.11480.024.543.02Union{Missing, Float64}
8gitlab2.2368411.012722Union{Missing, Int64}
9assessment0.49736100.012Union{Missing, Bool}
10exam_test16.4377-117.020383Union{Missing, Int64}
11date---CT 14:30383Union{Missing, String15}
12oral_exam46.898820.048.057.0419Union{Missing, Float64}
13veto1.011.01757Union{Missing, Bool}
14points_total80.323554.580.5105.0420Union{Missing, Float64}
15markAF383Union{Missing, String1}
16tutorIrenaPetr2Union{Missing, String7}
17percentil50.95131250.01000Int64
size(df2)
(760, 17)
nrow(df2)
760

2.1 Základní údaje

Nejprve prozkoumejte dostupné sloupce a pokuste se zjistit základní údaje jako

  • Kolik studentů mělo předmět zapsáno?
  • Kolik studentů získalo zápočet a dokončilo předmět?
  • Kolik studentů bylo v jakém ročníku? (V prvním běhu BI-MA1 toto nemá smysl.)
  • ...

Počet studentů.

nrow(df1)
528
nrow(df2)
760

Kolik studentů má zápočet?

sum(df1.assessment)
209
length(filter(isone, df1.assessment))
209
sum(df2.assessment)
missing
df2.assessment
760-element Vector{Union{Missing, Bool}}:
  true
  true
 false
 false
 false
 false
  true
  true
  true
  true
 false
  true
 false
     ⋮
  true
 false
 false
  true
  true
  true
  true
 false
  true
  true
 false
  true
filter(x -> !ismissing(x), df2.assessment)
758-element Vector{Union{Missing, Bool}}:
  true
  true
 false
 false
 false
 false
  true
  true
  true
  true
 false
  true
 false
     ⋮
  true
 false
 false
  true
  true
  true
  true
 false
  true
  true
 false
  true
filter(x -> !ismissing(x), df2.assessment) |> sum
377
sum(dropmissing(df2, :assessment).assessment)
377
coalesce.(df2.assessment, 0) |> sum
377

Tj. průchodnost zápočtu.

sum(df1.assessment) / nrow(df1)
0.3958333333333333
sum(coalesce.(df2.assessment, 0)) / nrow(df2)
0.49605263157894736

Počet vet v tabulce.

sum(df1.veto)
missing
missing + 1 # aha! pozor na missing hodnoty
missing
sum(dropmissing(df1[:, Cols(:veto)]).veto)
3
sum(filter(x -> !ismissing(x), df1.veto))
3
sum(dropmissing(df1, :veto).veto)
3
sum(dropmissing(df2, :veto).veto)
3

Získalo zápočet a úspěšně dokončilo předmět:

length(filter(x -> x != "F", dropmissing(df1, :mark).mark))
189
sum(dropmissing(df1, :mark).mark .!= "F")
189
sum(dropmissing(df2, :mark).mark .!= "F")
340

2.2 Zápočtové písemky, kvízy a semestr

Poté se podívejme podrobněji jak probíhalo získávání zápočtů.

  • Prozkoumejte výsledky jednotlivých zápočtových písemek, vytvořte historgramy výsledků.
  • Kolika studentům zápočet "těsně" unikl?
  • Kolik studentů "odpadlo" už v první polovině semestru?
  • ...
using PyPlot, Statistics

Histogram prvního zápočtové testu.

plt.grid()
hist(filter(x -> !ismissing(x), df1.test1), bins=[ -0.49 + j for j in 0:21]);
skipmissing(df1.test1)
skipmissing(Union{Missing, Float64}[2.0, 9.0, 6.0, 1.0, 4.0, 2.0, 9.5, 8.0, 12.0, 0.0  …  0.0, 1.0, 7.0, missing, 9.5, 5.5, 14.5, 10.5, 12.5, 13.0])
collect(skipmissing(df1.test1))
427-element Vector{Float64}:
  2.0
  9.0
  6.0
  1.0
  4.0
  2.0
  9.5
  8.0
 12.0
  0.0
  9.0
 17.5
  7.5
  ⋮
  2.5
  6.5
  8.0
  0.0
  1.0
  7.0
  9.5
  5.5
 14.5
 10.5
 12.5
 13.0
mean(skipmissing(df1.test1))
8.451990632318502
median(skipmissing(df1.test1))
8.0
length(collect(skipmissing(df1.test1)))
427
nrow(df1) - length(df1.test1[ismissing.(df1.test1)])
427

V druhém běhu MA1 nelze přímo porovnávat první testy.

plt.grid()
hist(filter(x -> !ismissing(x), df2.test1), bins=[ -0.49 + j for j in 0:4]);

Histogram druhého zápočtového testu.

plt.grid()
hist(filter(x -> !ismissing(x), df1.test2), bins=[ -0.49 + j for j in 0:21]);

Druhý a třetí test v druhém běhu MA1.

plt.grid()
hist(filter(x -> !ismissing(x), df2.test2), bins=[ -0.49 + j for j in 0:21]);
plt.grid()
hist(filter(x -> !ismissing(x), df2.test3), bins=[ -0.49 + j for j in 0:21]);
df1.test2 |> skipmissing |> collect |> length
309
df2.test2 |> skipmissing |> collect |> length
628
df2.test3 |> skipmissing |> collect |> length
515

Počet studentů, kteří získali zápočet bez opravné zápočtové písemky.

sum(coalesce.(df1.test1 + df1.test2, 0) .>= 25.)
165
sum(coalesce.(df2.test1 + df2.test2 + df2.test3, 0) .>= 25.)
325

Počet studentů s alespoň 20 body ze zápočtových písemek.

nrow(df1[coalesce.(df1.tests_total .>= 20, false), :])
234
nrow(df2[coalesce.(df2.tests_total .>= 20, false), :])
418

Histogram opravné zápočtové písemky.

plt.grid()
hist(filter(x -> !ismissing(x), df1.second_chance), bins=[ -0.49 + j for j in 0:25]);
plt.grid()
hist(filter(x -> !ismissing(x), df2.second_chance), bins=[ -0.49 + j for j in 0:25]);

Histogram celkového počtu bodů ze semestru.

plt.grid()
hist(filter(x -> !ismissing(x), df1.tests_total), bins=[ -0.49 + j for j in 0:41]);
plt.grid()
hist(filter(x -> !ismissing(x), df2.tests_total), bins=[ -0.49 + j for j in 0:41]);

Histogram celkového počtu bodů z písemek ze semestru ("zub milosti").

plt.grid()
hist(filter(x -> !ismissing(x), df1.points_total), bins=[ -0.49 + j for j in 55:106]);
plt.grid()
hist(filter(x -> !ismissing(x), df2.points_total), bins=[ -0.49 + j for j in 55:106]);

Úspěšnost zisku zápočtu podle cvičících.

df = combine(groupby(df1, :tutor),
    nrow => "studenti",
    :assessment => sum => "zápočty",
    :test1 => (x -> sum(coalesce.(x, 0) .> 0)) => "psali 1. test"
)
df[:, "propustnost"] = df[:, "zápočty"] ./ df[:, "studenti"]
df

7 rows × 5 columns

tutorstudentizápočtypsali 1. testpropustnost
String7?Int64Int64Int64Float64
1Honza8826580.295455
2Irena9530630.315789
3Jitka9534780.357895
4Pavel9638780.395833
5missing8000.0
6Tomáš9869900.704082
7Ivo4812390.25
df = combine(groupby(df2, :tutor),
    nrow => "studenti",
    :assessment => (x -> sum(collect(skipmissing(x)))) => "zápočty",
    :test1 => (x -> sum(coalesce.(x, 0) .> 0)) => "psali 1. test"
)
df[:, "propustnost"] = df[:, "zápočty"] ./ df[:, "studenti"]
df

9 rows × 5 columns

tutorstudentizápočtypsali 1. testpropustnost
String7?Int64Int64Int64Float64
1Jan V146641150.438356
2Pavel149841200.563758
3Jitka7341620.561644
4Petr9939640.393939
5Jiřina6836540.529412
6Irena7441680.554054
7Jarda10057900.57
8Jan S4915330.306122
9missing2000.0

2.3 Zkouškové období

V BI-MA1 hodně rozdílné vůči BI-ZMA.

Histogram známek.

combine(groupby(df1, :mark), nrow => "studenti") |> sort

7 rows × 2 columns

markstudenti
String1?Int64
1A42
2B57
3C53
4D32
5E5
6F20
7missing319
combine(groupby(df2, :mark), nrow => "studenti") |> sort

7 rows × 2 columns

markstudenti
String1?Int64
1A79
2B104
3C110
4D41
5E6
6F37
7missing383
sort(combine(groupby(df1[coalesce.(df1.gitlab, 0) .> 0 ,:], :mark), nrow => "studenti"), :mark)

6 rows × 2 columns

markstudenti
String1?Int64
1A12
2B7
3C8
4D2
5F4
6missing5
sort(combine(groupby(df2[coalesce.(df2.gitlab, 0) .> 0 ,:], :mark), nrow => "studenti"), :mark)

6 rows × 2 columns

markstudenti
String1?Int64
1A13
2B12
3C4
4D1
5F2
6missing6
sort(combine(groupby(df1[coalesce.(df1.activity, 0) .> 2 ,:], :mark), nrow => "studenti"), :mark)

6 rows × 2 columns

markstudenti
String1?Int64
1A29
2B25
3C13
4D10
5F4
6missing15
sort(combine(groupby(df2[coalesce.(df2.activity, 0) .> 2 ,:], :mark), nrow => "studenti"), :mark)

6 rows × 2 columns

markstudenti
String1?Int64
1A47
2B39
3C32
4D6
5F7
6missing28
plt.grid()
hist(filter(x -> !ismissing(x), df1.oral_exam), bins=[ -0.49 + j for j in 0:61]);
plt.grid()
hist(filter(x -> !ismissing(x), df2.oral_exam), bins=[ -0.49 + j for j in 0:58]);
plt.grid()
hist(filter(x -> !ismissing(x), df1.exam_test), bins=[ -0.49 + j for j in 0:21]);
plt.grid()
hist(filter(x -> !ismissing(x), df2.exam_test), bins=[ -0.49 + j for j in 0:21]);
combine(groupby(df1, :exam_test), nrow => "počet") |> sort

13 rows × 2 columns

exam_testpočet
Int64?Int64
104
2101
3111
4123
5132
6145
71529
81637
91740
101837
111934
122016
13missing319
combine(groupby(df1[df1.assessment .=== true, :], :exam_test), nrow => "počet") |> sort

12 rows × 2 columns

exam_testpočet
Int64?Int64
104
2101
3111
4123
5132
6145
71529
81637
91740
101837
111934
122016

Reference

Další informace, detaily či tutoriály lze nalézt v dokumentaci DataFrames.jl. V notebooku jsme používali balíček CSV.jl.