Skip to content

Instantly share code, notes, and snippets.

@Caellian
Last active June 9, 2025 13:10
Show Gist options
  • Save Caellian/aec637bbdd20c015287d0d01f6cab08e to your computer and use it in GitHub Desktop.
Save Caellian/aec637bbdd20c015287d0d01f6cab08e to your computer and use it in GitHub Desktop.
OVIS - Seminar
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.stats import skew, kurtosis, mode
file_path = "data.csv"
with open(file_path, "r") as f:
line = next(f)
data = list(map(lambda it: int(it.strip()), line.split(",")))
n = len(data)
print(f"Veličina uzorka: {n}")
print(f"Prvih 10 podataka: {data[:10]}")
bins = np.histogram_bin_edges(data, bins=10)
categories = pd.cut(data, bins)
frequency_table = categories.value_counts().sort_index()
rel_frequency_table = frequency_table / n
print("\nTablica frekvencija:")
freq_df = pd.DataFrame(
{
"Razred": frequency_table.index.astype(str),
"Frekvencija": frequency_table.values,
"Relativna frekvencija (%)": (rel_frequency_table.values * 100).round(2),
}
)
print(freq_df.to_string(index=False))
plt.figure(figsize=(12, 5))
# Histogram frekvencija
plt.subplot(1, 2, 1)
sns.histplot(data, bins=bins, kde=False)
plt.axvline(np.mean(data), color="red", linestyle="--", label="Sredina")
plt.axvline(np.median(data), color="green", linestyle="--", label="Medijan")
plt.axvline(mod_value, color="blue", linestyle="--", label="Mod")
plt.title("Histogram frekvencija")
plt.xlabel("Vrijednosti")
plt.ylabel("Frekvencija")
plt.legend()
# Histogram relativnih frekvencija
plt.subplot(1, 2, 2)
sns.histplot(data, bins=bins, kde=False, stat="probability")
plt.axvline(np.mean(data), color="red", linestyle="--", label="Sredina")
plt.axvline(np.median(data), color="green", linestyle="--", label="Medijan")
plt.axvline(mod_value, color="blue", linestyle="--", label="Mod")
plt.title("Histogram relativnih frekvencija")
plt.xlabel("Vrijednosti")
plt.ylabel("Relativna frekvencija")
plt.legend()
plt.tight_layout()
plt.show()
mean_value = np.mean(data)
print(f"\nAritmetička sredina: {mean_value:.2f}")
mod_value = mode(data, keepdims=True).mode[0]
median_value = np.median(data)
print(f"Mod: {mod_value}")
print(f"Medijan: {median_value}")
data_range = np.max(data) - np.min(data)
variance = np.var(data, ddof=1)
std_dev = np.std(data, ddof=1)
print(f"Raspon: {data_range:.2f}")
print(f"Varijanca: {variance:.2f}")
print(f"Standardna devijacija: {std_dev:.2f}")
asimetrija = skew(data)
zaobljenost = kurtosis(data)
print(f"Asimetrija (Skewness): {asimetrija:.4f}")
print(f"Zaobljenost (Kurtosis): {zaobljenost:.4f}")
if asimetrija > 0:
print("Distribucija je desno asimetrična.")
elif asimetrija < 0:
print("Distribucija je lijevo asimetrična.")
else:
print("Distribucija je simetrična.")
if zaobljenost > 0:
print("Distribucija je šiljatija od Gaussove.")
elif zaobljenost < 0:
print("Distribucija je plosnatija od Gaussove.")
else:
print("Distribucija ima zaobljenost sličnu Gaussovoj.")
#import "template/template.typ": appendix, config, figure-list, horizontal-page
#import "@preview/tablex:0.0.9": *
#import "@preview/oxifmt:0.2.1": strfmt
#import "@preview/cetz:0.3.2": canvas, draw
#import "@preview/cetz-plot:0.1.1": plot
#import "./algorithmic/algorithmic.typ"
#import algorithmic: algorithm, algorithm-figure
#show: config(
"seminar",
"Primjena deskriptivne statistike u analizi stvarnih podataka pomoću Pythona",
(
"Mihael Pokorni",
"Florijan Skender",
"Sven Šestak",
"Tin Švagelj",
),
attributions: [
*Voditeljica kolegija:* izv. prof. dr. sc. Marija Maksimović
],
summary: none,
//print-version: true,
)
#let todo(msg) = box(fill: yellow, inset: 4pt, text(fill: red, weight: "black", size: 1.2em, [TODO: #msg]))
#let round-fixed(value, decimals: 2) = {
let d = calc.pow(10, decimals)
calc.round(value * d) / d
}
#let var(..args) = $"Var"(#args.pos().join(","))$
#let stddev(..args) = $sigma(#args.pos().join(","))$
= Uvod
#let data = csv("kod/data.csv").at(0).map(it => int(it.trim()))
#let min = calc.min(..data)
#let max = calc.max(..data)
#let data-range = max - min
#let sample-count = data.len()
#let rank-count = 10
#let rank-step = data-range/rank-count
Za ovaj seminarski rad korišteni su podaci o broju zgrada za koje su izdane građevinske dozvole (novogradnja i dogradnja) kao populacija. Podaci su prikazani po mjesecima, od siječnja 2010. do prosinca 2024. godine, što čini uzorak od ukupno #sample-count mjesečnih zapisa.
#let table-items = {
let result = ()
for y in range(15) {
result.push([#(2010+y).])
let subseq = data.slice(y*12, (y+1)*12)
result += subseq.map(it => str(it))
result.push(hlinex())
}
result
}
Podaci su prikazani u tablici:
#figure(caption: [Broj zgrada za koje su izdane građevinske dozvole (novogradnja i dogradnja) @dzs2025dozvole], kind: table)[
#tablex(
columns: (5em,) + (1fr,) * 12,
align: (horizon+right,) + (horizon+center,) * 12,
auto-vlines: false,
auto-hlines: false,
hlinex(),vlinex(),
rowspanx(2, cellx(align: horizon+center)[Godina]),vlinex(),colspanx(12)[Mjesec],
hlinex(start:1),[1],hlinex(),vlinex(),[2],vlinex(),[3],vlinex(),[4],vlinex(),[5],vlinex(),[6],vlinex(),[7],vlinex(),[8],vlinex(),[9],vlinex(),[10],vlinex(),[11],vlinex(),[12],vlinex(),
..table-items
)
]
= Deskriptivna statistika
Kako bi se mogla napraviti tablica frekvencija i relativnih frekvencija, prvo je potrebno grupirati podatke u razrede. Primjetljivo je da su brojevi relativno sličnih vrijednosti te zbog toga je linearna distribucija razreda vrijednosti prikladna.
Najmanja vrijednost u skupu podataka je #math.equation(str(min)), a najveća #math.equation(str(max)). Prema tome, korak između razreda treba biti:
#figure(caption: [Korak između grupa brojeva],
$
(x_"max" - x_"min")/"br. rangova" = (#max - #min) / #rank-count = #rank-step.
$
)
Tablicu frekvencija za individualne vrijednosti u ovom slučaju nije smisleno raditi jer je frekvencija individualnih vrijednosti uzorka niska. U @tabl-freq[tablici] je stoga prikazana frekvencija i relativna frekvencija rangova vrijednosti. Grupacija vrijednosti u rangove nije neophodna u slučaju kada je korak između vrijednosti jednak, a raspon vrijednosti je smisleno malen.
#let grouped = ()
#let range-borders = ()
#let histogram-data = ()
#let histogram-freq-data = ()
#let mod = ()
#{
let max-width = 0.25
let max-items = 0
for g in range(rank-count) {
let group-min = round-fixed(min + g*rank-step, decimals: 1)
let group-max = round-fixed(group-min + rank-step, decimals: 1)
let group-mean = (group-max + group-min) / 2
let items = ()
for e in data {
if e > group-min and e <= group-max or (e == group-min and group-min == min) {
items.push(e)
}
}
if items.len() > max-items {
mod = (group-mean,)
max-items = items.len()
} else if items.len() == max-items {
mod.push(group-mean)
}
let description = (decimals) => []
let border-entry(val) = {
(val, move(dx: 5pt, dy: 6pt, rotate(30deg, str(val))))
}
if group-min == min {
range-borders.push(border-entry(group-min))
range-borders.push(border-entry(group-max))
description = (d) => $bracket.l #strfmt("{:.decimals$}", group-min, decimals: d) , #strfmt("{:.decimals$}", group-max, decimals: d) bracket.r$
} else {
range-borders.push(border-entry(group-max))
description = (d) => $angle.l #strfmt("{:.decimals$}", group-min, decimals: d) , #strfmt("{:.decimals$}", group-max, decimals: d) bracket.r$
}
grouped.push(description(1))
grouped.push(str(items.len()))
histogram-data.push((group-mean, items.len()))
let relative = items.len() / sample-count
let label = strfmt("{:.2}%", relative * 100)
histogram-freq-data.push((group-mean, relative))
grouped.push(math.equation(label))
}
}
#figure(caption: [Frekvencije i relativne frekvencije vrijednosti], kind: table)[
#tablex(
columns: (auto, auto, auto),
align: center+horizon,
[Razred], [Frekvencija], [Relativna frekvencija],
..grouped
)
] <tabl-freq>
Na osnovu frekvencija i relativnih frekvencija prikazanih u @tabl-freq[tablici], moguće je izraditi histograme koji vizualno prikazuju oblik distribucije vrijednosti:
#let mean = {
let sum = 0
for e in data {
sum += e
}
round-fixed(sum / sample-count)
}
#let median = {
let sorted = data.sorted()
let length = sorted.len()
if calc.even(sorted.len()) {
(sorted.at(int(length / 2)) + sorted.at(int(length / 2) + 1)) / 2
} else {
sorted.at(int(calc.ceil(length / 2)))
}
}
#let raw-style = (
stroke: none,
fill: gradient.linear(blue.lighten(70%), blue.lighten(80%))
)
#let freq-style = (
stroke: none,
fill: gradient.linear(red.lighten(70%), red.lighten(80%))
)
#figure(caption: [Histogrami frekvencija i relativnih frekvencija])[
#tablex(
stroke: none,
align: center,
)[
#canvas({
plot.plot(
size: (15, 5),
x-min: 260,
x-max: max,
x-tick-step: none,
x-ticks: range-borders,
x-label: [#v(1em) Br. građevinskih dozvola],
y-label: [Frekvencija],
legend: "inner-north-east", {
plot.add-bar(
histogram-data,
style: raw-style,
bar-width: rank-step,
)
plot.add-vline(mean, style: (stroke: (paint: green.lighten(10%), thickness: 3pt, dash: (5pt, 2pt))), label: "Artimetička sredina")
for m in mod {
plot.add-vline(m, style: (stroke: (paint: yellow.darken(30%), thickness: 2pt, dash: (0.1pt, 5pt), cap: "round")), label: "Mod")
}
plot.add-vline(median, style: (stroke: (paint: purple.lighten(40%), thickness: 2pt, dash: (5pt, 5pt))), label: "Medijan")
}
)
})
][a)][
#canvas({
plot.plot(
size: (15, 5),
x-min: 260,
x-max: max,
x-tick-step: none,
x-ticks: range-borders,
x-label: [#v(1em) Br. građevinskih dozvola],
y-label: [Relativna frekvencija],
legend: "inner-north-east", {
plot.add-bar(
histogram-freq-data,
style: freq-style,
bar-width: rank-step,
)
plot.add-vline(mean, style: (stroke: (paint: green.lighten(10%), thickness: 3pt, dash: (5pt, 2pt))), label: "Artimetička sredina")
for m in mod {
plot.add-vline(m, style: (stroke: (paint: yellow.darken(30%), thickness: 2pt, dash: (0.1pt, 5pt), cap: "round")), label: "Mod")
}
plot.add-vline(median, style: (stroke: (paint: purple.lighten(40%), thickness: 2pt, dash: (5pt, 5pt))), label: "Medijan")
}
)
})
][b)]
]<histograms>
Moguće je zamijetiti da su histogram frekvencija (a) i histogram relativnih frekvencija (b), prikazani na @histograms[slici], istog oblika te se razlikuju samo po skali y-osi. To je očekivano jer histogram relativnih frekvencija (b) nastaje tako što se vrijednost svakog ranga *uniformno skalira* kako bi zbroj svih vrijednosti bio jednak $1$.
Neophodno je izračunati aritmetičku sredinu uzorka za određivanje daljnjih heuristika:
#figure(caption: [aritmetička sredina uzorka],
$
#overline[x] = (sum_(i = 0)^n x_i)/n = (#data.at(0) + #data.at(1) + dots + #data.last())/#sample-count approx #mean.
$
)<mean-formula>
U kodu je izračun aritmetičke sredine jednako jednostavna procedura kao i u algebarskom obliku:
#algorithm-figure([procedura za izračun aritmetičke sredine], {
import algorithmic: *
Procedure(
"aritmeticka_sredina",
("niz",),
{
Assign[zbroj][$0$]
LineBreak
For(
[*svaki* $x in "niz"$],
{
Assign([zbroj], $"zbroj" + x$)
},
)
LineBreak
Return[$"zbroj" / K("niz")$]
},
)
})<algo-mean>
Iz @mean-formula iščitavamo da je kroz period uzorka prosječni mjesečni broj izdanih građevinskih dozvola $#mean approx #calc.round(mean)$.
Aritmetička sredina je prosječna vrijednost podataka. Zbog toga je osjetljiva na ekstreme i nije uvijek najbolje mjerilo za određivanje stvarne sredine distribucije.
Kako bi se preciznije odredile karakteristike distribucije, korisno je odrediti i vrijednosti moda i medijana. Mod je najčešća vrijednost u distribuciji, tj. u ovom slučaju najčešći razred. A medijan je središnja vrijednost kada se razmatraju sortirani podaci.
Procedura za računalni izračun moda je sljedeća:
#algorithm-figure([procedura za izračun moda], {
import algorithmic: *
Procedure(
"mod",
("niz",),
{
Comment[preslikavanje za praćenje kardinalnosti]
Assign[k][$emptyset$]
LineBreak
Comment[prebrojavanje]
For(
[*svaki* $x in "niz"$],
{
IfElseChain([$x in.not "dom"(k)$], {
Assign[k][$k union {(x, 1)}$]
}, {
Comment[povećanje vrijednosti preslikavanja za 1]
Assign[k][$k without {(x, k(x))} union {(x, k(x) + 1)}$]
})
},
)
LineBreak
Return[$"max" {k(x) : x in "dom"(k)}$]
},
)
})<algo-mod>
Procedura za računalni izračun medijana je sljedeća:
#algorithm-figure([procedura za izračun moda], {
import algorithmic: *
Procedure(
"medijan",
("niz",),
{
Assign[niz][*sortiraj*(niz)]
LineBreak
Comment[ako je kardinalnost parna onda vraćamo aritmetičku sredinu središnjih dviju vrijednosti]
IfElseChain($floor(K("niz") / 2) = K("niz") / 2$, {
State[]
Return[$("niz"[floor(K("niz") / 2)] + "niz"[floor(K("niz") / 2) + 1]) / 2$]
}, {
Return[$"niz"[K("niz") / 2]$]
})
},
)
})<algo-mod>
Uzorak je unimodalan, tj. ima samo jedan mod, a taj mod uzorka je #mod.at(0).
Medijan je #strfmt("{:.2}", median).
Medijan i mod se u ovom slučaju nalaze vrlo blizu jedan drugome na osnovu čega se može zaključiti da su podaci relativno simetrični.
Vrijedi $#overline[x] < "medijan" < "mod"$, pa može se zaključiti da je distribucija vrlo blago desno asimetrična.
Raspon vrijednosti za odabrani uzorak je #data-range.
#let variance = {
let up = 0
for e in data {
up += (e - mean) * (e - mean)
}
up / (sample-count - 1)
}
Radi se o uzorkovanim podacima, a ne teoretskim, te se zbog toga primjenjuje formula za varijancu:
#figure(caption: [varijanca uzorka],
$
var(x) = 1/(n-1) sum_(i=0)^n (x_i - #overline[x])
$
)<variance-formula>
Uvrštavanjem podataka uzorka u @variance-formula dobivamo da je:
#figure(caption: [izračun varijance],
$
var(x) = ((#data.at(0) - #mean)^2+(#data.at(1) - #mean)^2+dots.c+(#data.last() - #mean)^2)/(#sample-count - 1) = #round-fixed(variance)
$
)
U algoritamskom obliku taj izračun izgleda kao:
#algorithm-figure([procedura za izračun varijance], {
import algorithmic: *
Function(
"udaljenost",
("niz", "exp", "frac"),
{
Assign[rezultat][0]
Assign([sredina], FnInline([aritmeticka_sredina], ("niz")))
For([*svaki* $x in "niz"$], {
Assign[rezultat][$"rezultat" + (x - "sredina")^exp / "frac"$]
})
Return[rezultat]
}
)
LineBreak
Procedure(
"varijanca",
("niz",),
{
Return[$#FnInline("udaljenost", ("niz", $1$, $1$)) / (K("niz") - 1)$]
},
)
})<algo-mod>
#let std-dev = round-fixed(calc.sqrt(variance))
Standardna devijacija je korijen varijance:
#figure(caption: [standardna devijacija uzorka],
$
stddev(x) = sqrt(var(x)) = #math.sqrt[#round-fixed(variance)] = #std-dev
$
)
Jer je uzorak malen, Fisherove formule za koeficijent asimetrije $g_1$ i koeficijent zaobljenosti $g_2$ će dati bolje rezultate od Pearsonove kurtoze (engl. _Pearson's Kurtosis_), a uzorak je dovoljno velik da nije potrebno primjenjivati nepristranu kurtozu (engl. _Unbiased Kurtosis_):
#let assimtery = {
let n = sample-count
let up = 0
for e in data {
up += calc.pow((e - mean) / std-dev, 3)
}
n * up / (n - 1) / (n - 2)
}
#let kurtosis = {
let n = sample-count
let up = 0
for e in data {
up += calc.pow((e - mean) / std-dev, 4)
}
n * (n+1) * up / (n - 1) / (n - 2) / (n - 3)
}
#figure(caption: [Fisherove formule za koeficijent asimetrije],
$
g_1=(n)/((n-1)(n-2)) sum_(i=0)^n ((x_i-#overline[x])/s)^3=#round-fixed(assimtery, decimals: 4)\
g_2=(n(n+1))/((n-1)(n-2)(n-3)) sum_(i=0)^n ((x_i-#overline[x])/s)^4 = #round-fixed(kurtosis, decimals: 4)
$
)
Distribucija je blago desno asimetrična jer je $g_1 > 0$, te plosnatija od Gaussove (platokurtna) jer je $g_2 < 3$.
Koeficijent asimetrije ($g_1$) opisuje koliko je raspodjela podataka nagnuta u odnosu na srednju vrijednost.
Ako je koeficijent jednak nuli, raspodjela je simetrična.
Pozitivan koeficijent ukazuje na desnu asimetriju te znači da većina podataka ima niže vrijednosti, dok se rep distribucije proteže prema većim vrijednostima.
Negativan koeficijent označava lijevu asimetriju – većina podataka je viša, a rep ide prema manjim vrijednostima.
Koeficijent zaobljenosti ($g_2$) mjeri koliko je raspodjela “šiljata” ili “ravna” u usporedbi s normalnom raspodjelom.
Ako je vrijednost blizu 3, raspodjela ima oblik sličan normalnoj.
Vrijednosti veće od 3 ukazuju na oštriji vrh (leptokurtna distribucija), dok vrijednosti manje od 3 znače da je raspodjela plosnatija (platokurtna), s manjom koncentracijom podataka oko sredine i debljim repovima.
= Zaključak
U ovom seminarskom radu smo proveli deskriptivnu statističku analizu stvarnih podataka o broju izdanih građevinskih dozvola od siječnja 2010. do prosinca 2024. godine. Ukupno je analizirano #sample-count mjesečnih zapisa.
Izračunate su sljedeće deskriptivne mjere:
- Aritmetička sredina (#mean) predstavlja prosječnu mjesečnu vrijednost dozvola.
- Medijan (#strfmt("{:.2}", median)) je srednja vrijednost sortirane distribucije i korisna je za opisivanje stvarnog “središta” u prisustvu odstupanja.
- Mod (#mod.at(0)) označava najčešće pojavljivani razred vrijednosti u uzorku te sugerira tipičan broj dozvola u mjesecu.
- Raspon (#data-range) prikazuje širinu podataka, tj. razliku između najveće i najmanje vrijednosti.
- Varijanca (#round-fixed(variance)) opisuje prosječnu kvadratnu udaljenost vrijednosti od sredine, a time i ukupno raspršenje podataka.
- Standardna devijacija (#std-dev) je korijen varijance koji opisuje prosječno odstupanje.
- Koeficijent asimetrije (g₁) (#round-fixed(assimtery, decimals: 4)) pokazuje da je distribucija blago desno asimetrična, što znači da većina mjeseci ima manji broj izdanih dozvola, dok se manji broj mjeseci izdvaja s većim vrijednostima.
- Koeficijent zaobljenosti (g₂) (#round-fixed(kurtosis, decimals: 4)) ukazuje da je distribucija platokurtna, tj. ima manje izražen vrh od normalne distribucije i deblje repove.
Na temelju dobivenih rezultata može se reći da su podaci uglavnom ravnomjerno raspoređeni, ali ipak malo naginju prema većim vrijednostima. Budući da su mod i medijan vrlo blizu kao što je vidljivo na @histograms[slici], to pokazuje da su vrijednosti podataka prilično ujednačene i da nema velikih skokova ili iznimki.
Histogrami također pokazuju da su podaci prilično stabilni i da nema velikih oscilacija, već se većina vrijednosti nalazi oko sredine.
#pagebreak()
#appendix[
= Python kȏd
#let python = read("kod/code.py")
#raw(python, lang: "python")
]
#pagebreak()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment