V této lekci se ponoříme (zabředneme) do úplných základů Pythonu. Daná tématika se může zdát možná až příliš abstraktní a v praxi nepoužitelná, nicméně opak je pravdou! Uvidíme, že díky solidním základům bude například načítání dat ze souboru jednoduchou a přímočarou záležitostí.
Na začátku zavzpomínáme na (dnes již klasický) Fišerův problém. Data jsme tehdy měli uložená v csv souboru data.csv. Vše jsme načetli jednoduše pomocí pandy:
In [1]:
import pandas as pd
data = pd.read_csv('data.csv')
data
Out[1]:
Pokud vzpomínáte, tak ke sloupci T jsme přistupovali takto:
In [2]:
data['T']
Out[2]:
Proč jsme nemohli jednoduše vykonat následující?
In [3]:
data[T]
In [4]:
T = 'T'
#alias v hodnota
Alias je (zhruba řečeno) pojmenované místo v paměti počítače. Tedy pomocí T označujeme při běhu programu místo v paměti, kam jsme si již předtím něco uložili (v tomto případě "písmeno" T). Ne vše může být aliasem (např. žádná proměnná nemůže začínat číslem):
In [5]:
3 = 'T'
Naopak následující příkaz:
In [6]:
300
Out[6]:
právě vytvořil nové "místo" v paměti počítače, kam uložil hodnotu (v binární podobě), která reprezentuje přirozené číslo 300. Že jsme si místo (jeho adresu) nezapamatovali, je náš problém. Nyní už neexistuje žádný způsob, jak zjistit, kde vlastně je ono místo a tudíž jej nemůžeme použít dále při běhu programu. Jupyter nám dokonce dává jasně najevo, že jsme si ono místo neuložili do proměnné (viz Out[6]: 300).
Tím se dostáváme k tomu, co vlastně znamená symbol = ve zdrojovém kódu. Jedná se o tzv. operátor přiřazení. Aliasu přiřazuje místo v paměti, na které "ukazuje". Rozeberme tedy podrobně, co znamená následující:
In [7]:
x = 1 # zde vznikne místo v paměti, do které se uloží číslo 1 a proměnná x ukazuje na to místo
x = x + 1 # zde se vezme obsah proměnné x (číslo 1) a provede se operace součtu,
# výsledek se uloží se do x
x # výsledek je pochopitelně 2
Out[7]:
Jinými slovy: to co je na pravé straně operátoru = se vyhodnotí (získají se hodnoty uložené v paměti), aplikují se všechny operace a adresu místa s výsledkem si uložíme do aliasu na levé straně.
Klasický, z matematiky známý, operátor rovnosti se v Pythonu označuje == (porovnává hodnoty uložené na daných místech v paměti)
In [8]:
300 == 300
Out[8]:
In [9]:
300 == 301
Out[9]:
Na identitu (tedy že daný alias ukazuje na stejné místo v paměti) se ptáme slůvkem is:
In [10]:
a = 300
b = 300
a is b # a ukazuje na jiné místo v paměti než b
Out[10]:
V obou místech je však uložená stejná hodnota (číslo 300), tedy:
In [11]:
a == b
Out[11]:
Řetězec (string) v Pythonu reprezentuje text. Python nerozlišuje mezi znakem (character) a sekvencí znaků (string). Znak je v Pythonu reprezentován jako string o velikosti 1:
In [12]:
T = 'T'
len(T) #Příkaz len vrací délku (v tomto případě délku řetězce)
Out[12]:
Nyní můžeme udělat to, co nám v Úvodu nefungovalo:
In [13]:
data[T] # T se vyhodnotí na písmeno "T"
Out[13]:
V Pythonu není rozdíl mezi jednoduchou uvozovkou ' a dvojitou ", následující definice jsou si ekvivalentní
In [14]:
T1 = 'T'
T2 = "T"
T3 = """T"""
T4 = '''T'''
T1 == T2 == T3 == T4
Out[14]:
Tři uvozovky (ať už jednoduché či dvojité) se můžou rozprostírat na víc řádků.
In [15]:
print('''
Hroch a Panda
jsou dobří přátelé
''')
Stringy lze opět porovnávat, v tomto případě použijeme operátor nerovnosti:
In [16]:
'Hroch' != 'Zikán'
Out[16]:
Naopak následující může být trochu překvapivé (Python pochopitelně porovnává jednotlivé znaky, sémantice nerozumí):
In [17]:
'hroch' == 'zvíře'
Out[17]:
Jako dobrá představa stringu je seznam (list) znaků. Vskutku, se stringem lze zacházet obdobně jako se seznamem:
In [18]:
s = 'Panda'
print(s[0])
print(s[1:5])
Dva či více stringů lze spojit pomocí operátoru +:
In [19]:
'Hroch' + ' a ' + 'Panda'
Out[19]:
Podobnost s listem však pokulhává v jedné zásadní věci:
In [20]:
s[0] = 'F'
In [21]:
l1 = ['a', 'a', 'n', 'd']
In [22]:
l1.append('a')
Jak už víte, prvky seznamu (narozdíl od stringu) můžeme měnit:
In [23]:
l1[0] = 'P'
In [24]:
l1
Out[24]:
Dokážete však vysvětlit následující?
In [25]:
l2 = l1
l2.append('!')
l1
Out[25]:
Jak je možné, že se změnil list l1, když jsme modifikovali l2? Pro jistotu:
In [26]:
l2 # l2 se změnil také
Out[26]:
Odpověď je poměrně jednoduchá - l1 i l2 ukazují na stejné místo v paměti (jedná se o stejné "objekty"):
In [27]:
l1 is l2
Out[27]:
Pokud bychom chtěli vytvořit opravdovou kopii, tak aby se nám změny neprojevily na l1:
In [28]:
l3 = l1.copy()
In [29]:
l3
Out[29]:
In [30]:
l3.append('!')
l1
Out[30]:
In [31]:
l3
Out[31]:
Pro jistotu se ještě přesvědčíme, že se opravdu nejedná o stejné objekty:
In [32]:
l3 is l1
Out[32]:
In [33]:
s = 'Panda'
s[1:]
Out[33]:
In [34]:
'F' + s[1:]
Out[34]:
Obdobně se chová i datový typ integer (celé číslo). Porovnejte následující s předchozími hrátky s listy l1 a l2:
In [35]:
a = 1
b = a
a = 2
print(a)
print(b)
Toto je velice důležité chování Pythonu. Pokud si do nějaké proměnné uložíte nějaké číslo, pak se nikdy nemůže změnit jinak, než že ho sami explicitně změníte!
Nemutanti:
Mutanti:
In [36]:
f = open('data.csv')
f
Out[36]:
Proměnná f nyní představuje tzv. file handle. Neobsahuje žádná data (ta zatím stále leží na disku), je to jenom prostředek, skrze který můžeme komunikovat s operačním systémem. Samotný transfer dat můžeme zahájit zavoláním funkce read:
In [37]:
data = f.read()
print(data)
Nyní máme data v operační paměti a uložili jsme si je do proměnné data. Nyní je záhodno sdělit operačnímu systému, že se souborem již nebudeme dále pracovat a soubor takzvaně uzavřít:
In [38]:
f.close()
Podívejme se blíže na proměnnou data:
In [39]:
print(type(data))
data
Out[39]:
Vidíme, že tentokrát máme k dispozici něco úplně jiného, než co nám vrátila panda v první lekci zavoláním funkce read_csv. Tentokrát se jedná o string, ony záhadné \n označují znak nového řádku, Python jim rozumí a pokud použijeme funkci print, pak se vše zobrazí správně (viz výše).
V této podobě jsou nicméně data velice špatně použitelná. Nezbývá nám nic jiného, než si data sami upravit. Existuje mnoho způsobů, jak to udělat. Ten který si teď ukážeme není sice nejelegantnější, je však velice názorný.
Nejprve data rozdělíme na jednotlivé řádky:
In [40]:
data = data.split('\n')
data
Out[40]:
Následně každý řádek rozdělíme podle čárky na jednotlivé hodnoty:
In [41]:
data = [line.split(',') for line in data if line != '']
data
Out[41]:
Naším cílem je vytvořit starý známý DataFrame. Po pozorném přečtení dokumentace vidíme, že nejprve potrebujeme vytvořit numpy vektor (v tomto případě 2D vektor - správně bychom mělí říci tenzor či matice):
In [42]:
import numpy as np
arr = np.array(data[1:])
arr
Out[42]:
2D numpy vektory, jsou velice podobné 1D vektorům, k prvnímu řádku této matice lze přistoupit takto:
In [43]:
arr[0]
Out[43]:
K prvnímu elementu prvního řádku pak takto:
In [44]:
arr[0][0]
Out[44]:
Nyní máme konečně vše nachystané k vytvoření DataFrame:
In [45]:
import pandas as pd
data = pd.DataFrame(arr, columns=data[0])
In [46]:
data
Out[46]:
In [47]:
data['C'] = data['A'] + data['B']
In [48]:
data
Out[48]:
Asi jsme něco udělali špatně. Co se to vlastně stalo?
In [49]:
def load_data(file_name):
f = open(file_name)
data = f.read()
f.close()
return data
def make_dataframe(data):
data = data.split('\n')
data = [line.split(',') for line in data if line != '']
arr = np.array(data[1:])
return pd.DataFrame(arr, columns=data[0])
In [50]:
data = make_dataframe(load_data('data.csv'))
data
Out[50]:
In [51]:
x = data['A'][0]
print(x)
print(type(x))
Problém spočívá v tom, že data jsou stringy, ne floaty. Musíme je ručně převést. K tomu slouží funkce float:
In [52]:
print(float(x))
print(type(float(x)))
Pokud bychom měli následující vnořenou strukturu
In [53]:
strings = [['1', '2', '4'], ['5', '9', '9']]
strings
Out[53]:
a chtěli vytvořit novou identickou strukturu jen převést všechny stringy na floaty, pak to můžeme udělat např. následovně:
In [54]:
floats = []
for rec in strings:
floats.append([float(num) for num in rec])
floats
Out[54]:
Poznamenejme jen, že se vlastně jedná o dva vnořené for cykly. V prvním procházíme prvky listu strings - cykly tedy budou dva: první s ['1', '2', '4'] a druhý s ['5', '9', '9']. Vnořený for cylkus bude nejprve pro hodnoty 1, 2 a 4 a následně pro 5, 9 a 9. Výsledekem každého vnitřního for cyklu bude vždy nový list: [1.0, 2.0, 4.0] a [5.0, 9.0, 9.0]. Tyto listy vždy přilepíme na konec listu float, který si na začátku inicializujeme na prázdný list.
Naši funkci make_dataframe tedy můžeme upravit například následujícím způsobem:
In [55]:
def make_dataframe(data):
data = data.split('\n')
data = [line.split(',') for line in data if line != '']
floats = []
for rec in data[1:]:
floats.append([float(num) for num in rec])
arr = np.array(floats)
return pd.DataFrame(arr, columns=data[0])
In [56]:
data = make_dataframe(load_data('data.csv'))
data
Out[56]:
In [57]:
print(type(data['A'][0]))
In [58]:
data['C'] = data['A'] + data['B']
data
Out[58]:
Poznámka: pokud bychom četli dokumentaci opravdu pozorně zjistili bychom, že panda za nás může konverzi provést sama:
In [59]:
def make_dataframe(data):
data = data.split('\n')
data = [line.split(',') for line in data if line != '']
arr = np.array(data[1:])
return pd.DataFrame(arr, columns=data[0], dtype='f')
In [60]:
data = make_dataframe(load_data('data.csv'))
data
Out[60]:
In [61]:
print(type(data['A'][0]))
In [62]:
with open('data.csv') as f:
data = []
for line in f:
row = line.strip().split(',')
data.append(row)
data = pd.DataFrame(np.array(data[1:]), columns=data[0], dtype='f')
data
Out[62]:
Uvedený způsob skýtá dvě velké výhody.
With block se automaticky postará o zavření souboru. I kdybychom udělali nějakou chybu v kódu ve with blocku, Python stejně automaticky zavře soubor, takže po nás nezůstane nic otevřeného (existuje samozřejmě způsob, jak to ošetřit i ručně, ale tím se zde zabývat nebudeme)
Důležitý rozdíl při iteraci přes soubor oproti použití funkce read je v tom, že jsme NENAČETLI celý obsah souboru naráz. Operační systém nám v každé iteraci vrátil pouze jeden řádek. My jsme si sice všechna data uložili do proměnné data, protože jsme na konec chtěli vytvořit DataFrame. Pokud bychom však chtěli např. pouze sečíst všechny hodnoty v prvním sloupci, mohli bychom to udělat například následovně:
In [63]:
total_sum = 0
with open('data.csv') as f:
f.readline() # zahodíme hlavičku
for line in f:
total_sum += float(line.split(',')[0])
total_sum
Out[63]:
Výhodou je, že takto lze zpracovat i soubory, které jsou větší než operační paměť našeho počítače. Pokud bychom se pokusili načíst takový soubor celý do paměti, tak se dostaneme do vážných problémů.