Kapr v medu

moto:

Spadne kapr do medu a říká:

"Hustý, to je hustý..."

Z.Janák, písemka z TM

Osnova

  • Úvod
  • Alias vs hodnota
  • String
  • Mutanti a nemutanti
  • Práce se souborem
  • Elegance pythonu
  • Závěrečné cvičení

Úvod

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]:
T A B
0 15.0 0.1734 459.0
1 16.0 0.1782 450.0
2 17.0 0.1831 441.0
3 18.0 0.1880 435.0
4 19.0 0.1928 427.0
5 20.0 0.1976 419.0
6 21.0 0.2024 411.0

Pokud vzpomínáte, tak ke sloupci T jsme přistupovali takto:


In [2]:
data['T']


Out[2]:
0    15.0
1    16.0
2    17.0
3    18.0
4    19.0
5    20.0
6    21.0
Name: T, dtype: float64

Proč jsme nemohli jednoduše vykonat následující?


In [3]:
data[T]


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-3-a7305c9cf76a> in <module>()
----> 1 data[T]

NameError: name 'T' is not defined

Alias v hodnota


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'


  File "<ipython-input-5-e237428ae1a1>", line 1
    3 = 'T'
           ^
SyntaxError: can't assign to literal

Naopak následující příkaz:


In [6]:
300


Out[6]:
300

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]:
2

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]:
True

In [9]:
300 == 301


Out[9]:
False

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]:
False

V obou místech je však uložená stejná hodnota (číslo 300), tedy:


In [11]:
a == b


Out[11]:
True

String

Ř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]:
1

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]:
0    15.0
1    16.0
2    17.0
3    18.0
4    19.0
5    20.0
6    21.0
Name: T, dtype: float64

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]:
True

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é
''')


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]:
True

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]:
False

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])


P
anda

Dva či více stringů lze spojit pomocí operátoru +:


In [19]:
'Hroch' + ' a ' + 'Panda'


Out[19]:
'Hroch a Panda'

Podobnost s listem však pokulhává v jedné zásadní věci:


In [20]:
s[0] = 'F'


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-20-2e3ccb91d04c> in <module>()
----> 1 s[0] = 'F'

TypeError: 'str' object does not support item assignment

Mutanti

Souhrnným označením mutanti se označují datové typy, které se mohou měnit (mutable data type). Jeden takový už známe. Vzpomenete si který?


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]:
['P', 'a', 'n', 'd', 'a']

Dokážete však vysvětlit následující?


In [25]:
l2 = l1
l2.append('!')
l1


Out[25]:
['P', 'a', 'n', 'd', 'a', '!']

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]:
['P', 'a', 'n', 'd', 'a', '!']

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]:
True

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]:
['P', 'a', 'n', 'd', 'a', '!']

In [30]:
l3.append('!')
l1


Out[30]:
['P', 'a', 'n', 'd', 'a', '!']

In [31]:
l3


Out[31]:
['P', 'a', 'n', 'd', 'a', '!', '!']

Pro jistotu se ještě přesvědčíme, že se opravdu nejedná o stejné objekty:


In [32]:
l3 is l1


Out[32]:
False

Nemutanti

Jak jsme viděli před chvílí, stringy nelze modifikovat. Pokud bychom chtěli změnit string 'Panda' na 'Fanda', nezbyde nám nic jiného než vytvořit string nový:


In [33]:
s = 'Panda'
s[1:]


Out[33]:
'anda'

In [34]:
'F' + s[1:]


Out[34]:
'Fanda'

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)


2
1

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!

Mutanti vs nemutanti

Nemutanti:

  • int (-5, 2, 150333)
  • float (-5.1, -2., 1.50333e5)
  • string ('Hroch')
  • ...

Mutanti:

  • list ([1, 2])
  • DataFrame
  • vektor
  • ...
  • ...

Práce se souborem

Soubor označuje a sdružuje data uložená na disku. Abychom se k datům v souboru dostali a mohli s nimi pracovat, musíme požádat operační systém. Nebojte se, v Pythonu to není nic složitého. Prvním nezbytným krokem je soubor otevřít:


In [36]:
f = open('data.csv')
f


Out[36]:
<_io.TextIOWrapper name='data.csv' mode='r' encoding='UTF-8'>

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)


T,A,B
15.0,0.1734,459.0
16.0,0.1782,450.0
17.0,0.1831,441.0
18.0,0.1880,435.0
19.0,0.1928,427.0
20.0,0.1976,419.0
21.0,0.2024,411.0

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


<class 'str'>
Out[39]:
'T,A,B\n15.0,0.1734,459.0\n16.0,0.1782,450.0\n17.0,0.1831,441.0\n18.0,0.1880,435.0\n19.0,0.1928,427.0\n20.0,0.1976,419.0\n21.0,0.2024,411.0\n'

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]:
['T,A,B',
 '15.0,0.1734,459.0',
 '16.0,0.1782,450.0',
 '17.0,0.1831,441.0',
 '18.0,0.1880,435.0',
 '19.0,0.1928,427.0',
 '20.0,0.1976,419.0',
 '21.0,0.2024,411.0',
 '']

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]:
[['T', 'A', 'B'],
 ['15.0', '0.1734', '459.0'],
 ['16.0', '0.1782', '450.0'],
 ['17.0', '0.1831', '441.0'],
 ['18.0', '0.1880', '435.0'],
 ['19.0', '0.1928', '427.0'],
 ['20.0', '0.1976', '419.0'],
 ['21.0', '0.2024', '411.0']]

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]:
array([['15.0', '0.1734', '459.0'],
       ['16.0', '0.1782', '450.0'],
       ['17.0', '0.1831', '441.0'],
       ['18.0', '0.1880', '435.0'],
       ['19.0', '0.1928', '427.0'],
       ['20.0', '0.1976', '419.0'],
       ['21.0', '0.2024', '411.0']], 
      dtype='<U6')

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]:
array(['15.0', '0.1734', '459.0'], 
      dtype='<U6')

K prvnímu elementu prvního řádku pak takto:


In [44]:
arr[0][0]


Out[44]:
'15.0'

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]:
T A B
0 15.0 0.1734 459.0
1 16.0 0.1782 450.0
2 17.0 0.1831 441.0
3 18.0 0.1880 435.0
4 19.0 0.1928 427.0
5 20.0 0.1976 419.0
6 21.0 0.2024 411.0

In [47]:
data['C'] = data['A'] + data['B']

In [48]:
data


Out[48]:
T A B C
0 15.0 0.1734 459.0 0.1734459.0
1 16.0 0.1782 450.0 0.1782450.0
2 17.0 0.1831 441.0 0.1831441.0
3 18.0 0.1880 435.0 0.1880435.0
4 19.0 0.1928 427.0 0.1928427.0
5 20.0 0.1976 419.0 0.1976419.0
6 21.0 0.2024 411.0 0.2024411.0

Asi jsme něco udělali špatně. Co se to vlastně stalo?

Datové konverze

Projděme si celý proces načítání dat ještě jednou. Nadefinujme si k tomu dvě užitečné funkce:


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]:
T A B
0 15.0 0.1734 459.0
1 16.0 0.1782 450.0
2 17.0 0.1831 441.0
3 18.0 0.1880 435.0
4 19.0 0.1928 427.0
5 20.0 0.1976 419.0
6 21.0 0.2024 411.0

In [51]:
x = data['A'][0]
print(x)
print(type(x))


0.1734
<class 'str'>

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)))


0.1734
<class 'float'>

Pokud bychom měli následující vnořenou strukturu


In [53]:
strings = [['1', '2', '4'], ['5', '9', '9']]
strings


Out[53]:
[['1', '2', '4'], ['5', '9', '9']]

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]:
[[1.0, 2.0, 4.0], [5.0, 9.0, 9.0]]

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]:
T A B
0 15.0 0.1734 459.0
1 16.0 0.1782 450.0
2 17.0 0.1831 441.0
3 18.0 0.1880 435.0
4 19.0 0.1928 427.0
5 20.0 0.1976 419.0
6 21.0 0.2024 411.0

In [57]:
print(type(data['A'][0]))


<class 'numpy.float64'>

In [58]:
data['C'] = data['A'] + data['B']
data


Out[58]:
T A B C
0 15.0 0.1734 459.0 459.1734
1 16.0 0.1782 450.0 450.1782
2 17.0 0.1831 441.0 441.1831
3 18.0 0.1880 435.0 435.1880
4 19.0 0.1928 427.0 427.1928
5 20.0 0.1976 419.0 419.1976
6 21.0 0.2024 411.0 411.2024

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]:
T A B
0 15.0 0.1734 459.0
1 16.0 0.1782 450.0
2 17.0 0.1831 441.0
3 18.0 0.1880 435.0
4 19.0 0.1928 427.0
5 20.0 0.1976 419.0
6 21.0 0.2024 411.0

In [61]:
print(type(data['A'][0]))


<class 'numpy.float32'>

Elegance Pythonu

Vzhledem k tomu, že práce se souborem je velice častá, Python nabízí několik elegantních metod, jak si ulehčit život. Prvně si představíme tzv. "with block", který nás zbaví nutnosti soubor ručně zavírat a pak si ukážeme, jak efektivně iterovat přes jednotlivé řádky souboru.


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]:
T A B
0 15.0 0.1734 459.0
1 16.0 0.1782 450.0
2 17.0 0.1831 441.0
3 18.0 0.1880 435.0
4 19.0 0.1928 427.0
5 20.0 0.1976 419.0
6 21.0 0.2024 411.0

Uvedený způsob skýtá dvě velké výhody.

  1. 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)

  2. 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]:
126.0

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ů.

Závěrečné cvičení

Vytvořte (pomocí Pythonu pochopitelně) soubor s názvem data2.csv, který bude identický jako data.csv, jen přidáme čtvrtý sloupec, který vznikne vynásobením druhého a třetího sloupce. Tento sloupec pojmenujeme C.