Често използвана Python синтаксис в науката за данни (напреднало ниво)

През последните няколко дни разглеждах книгата Data Science from Scrach (PDF адрес), която е отлично и лесно разбираемо въведение в науката за данни. Една от главите представя както основите, така и по-напредналия синтаксис на Python, често използван в областта на науката за данни. Намирам изложението за много добро, кратко и ясно, затова реших да го преведа и публикувам тук като лична бележка. Често използвана Python синтаксис в науката за данни (основни понятия) Често използвана Python синтаксис в науката за данни (напреднало ниво)

Тази глава е посветена на представянето на по-сложен Python синтаксис и функционалности, които са изключително полезни при обработката на данни (базирани на Python 2.7).

Сортиране Sorting

Ако искате да сортирате списък в Python, можете да използвате метода sort на списъка. Ако не желаете да променяте оригиналния списък, използвайте функцията sorted, която връща нов, сортиран списък:

x = [4,1,2,3]
y = sorted(x)       # y = [1,2,3,4], x не се променя
x.sort()            # сега x = [1,2,3,4]
# По подразбиране sort или sorted сортират списъка във възходящ ред.

За низходящо сортиране може да се зададе параметър reverse = True.

Можете също така да дефинирате собствена функция за сортиране, за да подредите списъка по определен ключ:

# Сортиране по абсолютна стойност в низходящ ред
x = sorted([-4,1,-2,3], key=abs, reverse=True) # е [-4,3,-2,1]
# Сортиране по честота на думите в низходящ ред
wc = sorted(word_counts.items(),
key=lambda (word, count): count,
reverse=True)

Списъчни изрази List Comprehensions

Често възниква нуждата да извлечем конкретни елементи от списък, за да формираме нов, или да променим стойностите на някои от тях, или комбинация от двете. Идиоматичният начин за това в Python са списъчните изрази (List Comprehensions):

even_numbers = [x for x in range(5) if x % 2 == 0]  # [0, 2, 4]
squares = [x * x for x in range(5)]                 # [0, 1, 4, 9, 16]
even_squares = [x * x for x in even_numbers]        # [0, 4, 16]

По същия начин можете да преобразувате списъци в речници или множества:

square_dict = { x : x * x for x in range(5) }       # { 0:0, 1:1, 2:4, 3:9, 4:16 }
square_set = { x * x for x in [1, -1] }             # { 1 }

Ако не е нужно да използвате елементите на списъка, можете да използвате долна черта като променлива:

zeroes = [0 for _ in even_numbers] # с дължина, равна на списъка even_numbers

Списъчните изрази поддържат и множество for цикли:

pairs = [(x, y)
    for x in range(10)
    for y in range(10)]    # Общо 100 двойки: (0,0) (0,1) ... (9,8), (9,9)

Следващите for цикли могат да използват резултатите от предходните for цикли:

increasing_pairs = [(x, y)                      # съдържа само двойки, където x < y
                    for x in range(10)          # range(lo, hi) е равно на
                    for y in range(x + 1, 10)]  # [lo, lo + 1, ..., hi - 1]

В бъдеще често ще използваме списъчни изрази.

Генератори и итератори Generators and Iterators

Един проблем със списъците е, че могат да станат изключително големи, например range(1000000) ще генерира списък от един милион елемента. Ако обработвате данните един по един, това може да отнеме твърде много време (или да изчерпи паметта). В действителност може да се нуждаете само от първите няколко елемента, което прави останалите операции излишни.

Генераторите обаче ви позволяват да итерирате само през данните, от които се нуждаете. Можете да създадете генератор, използвайки функция и израза yield:

def lazy_range(n):
    """мързелива" версия на `range`"""
    i = 0
    while i < n:
        yield i
        i += 1

Бележка от преводача: Генераторът е специален вид итератор, а yield е ключов за реализирането на итерациите в него. Той служи като точка за пауза и възобновяване на изпълнението на генератора, като може да присвоява стойности на израза yield или да връща стойността му. Всяка функция, която съдържа оператор yield, се нарича генератор. Когато генераторът спре изпълнението си, той запазва текущото състояние и го възстановява при следващото извикване, за да произведе следващата итерирана стойност. Използването на списъци за итерация може да заеме голямо количество памет, докато генераторите използват значително по-малко, спестявайки ресурси.

Следващият цикъл ще консумира по една стойност от yield наведнъж, докато не бъдат изчерпани всички:

for i in lazy_range(10):
    do_something_with(i)

(Всъщност Python има вградена функция, която постига ефекта на горния _lazy_range_, наречена xrange, а в Python 3 е range.) Това означава, че можете да създадете безкрайна поредица:

def natural_numbers():
    """Връща 1, 2, 3, ..."""
    n = 1
    while True:
        yield n
        n += 1

Въпреки това не се препоръчва използването на подобни конструкции без логика за изход от цикъла.

TIP

Един недостатък на итерирането с генератори е, че елементите могат да бъдат итерирани само веднъж от началото до края. Ако искате да итерирате няколко пъти, трябва да създавате нов генератор всеки път или да използвате списък.

Вторият начин за създаване на генератор е чрез израз за разбиране в скоби:

lazy_evens_below_20 = (i for i in lazy_range(20) if i % 2 == 0)

Знаем, че методът items() на речниците връща списък с всички двойки ключ-стойност в речника, но по-често използваме метода на генератор iteritems() за итериране, който генерира и връща по една двойка ключ-стойност наведнъж.

Случайност Randomness

При изучаването на наука за данни често ще се налага да генерираме случайни числа, затова просто импортираме модула random и можем да го използваме:

import random
four_uniform_randoms = [random.random() for _ in range(4)]
# [0.8444218515250481,        # random.random() генерира случайно число
# 0.7579544029403025,         # Случайните числа са стандартизирани, в диапазона между 0 и 1
# 0.420571580830845,          # Тази функция е най-често използваната за генериране на случайни числа
# 0.25891675029296335]

Ако искате да получите възпроизводими резултати, можете да накарате модула random да генерира псевдослучайни (т.е. детерминирани) числа въз основа на вътрешното състояние, зададено от random.seed:

random.seed(10)           # задава сийд 10
print random.random()     # 0.57140259469
random.seed(10)           # нулира сийд 10
print random.random()     # 0.57140259469 отново

Понякога използваме и функцията random.randrange за генериране на случайно число в определен диапазон:

random.randrange(10)      # Избира случайно число от range(10) = [0, 1, ..., 9]
random.randrange(3, 6)    # Избира случайно число от range(3, 6) = [3, 4, 5]

Има и други методи, които са удобни понякога, например random.shuffle разбърква елементите в списък, генерирайки нов, случайно подреден списък:

up_to_ten = range(10)
random.shuffle(up_to_ten)
print up_to_ten
# [2, 5, 1, 9, 7, 3, 8, 6, 4, 0] (Вашият резултат вероятно ще бъде различен)

Ако искате да изберете случайно един елемент от списък, можете да използвате метода random.choice:

my_best_friend = random.choice(["Alice", "Bob", "Charlie"]) # Аз получих "Bob"

Ако искате да генерирате случайна поредица, но без да разбърквате оригиналния списък, можете да използвате метода random.sample:

lottery_numbers = range(60)
winning_numbers = random.sample(lottery_numbers, 6) # [16, 36, 10, 6, 25, 9]

Можете да изберете множество случайни проби (с повторение) чрез многократно извикване:

four_with_replacement = [random.choice(range(10))
                         for _ in range(4)]
# [9, 4, 4, 2]

Регулярни изрази Regular Expressions

Регулярните изрази се използват за търсене на текст. Те са малко сложни, но изключително полезни, затова има много книги, посветени само на тях. Ще ги обясняваме по-подробно, когато се сблъскаме с тях. Ето няколко примера за използване на регулярни изрази в Python:

import re
print all([                                 # Всички изрази по-долу връщат True, защото
    not re.match("a", "cat"),               # * 'cat' не започва с 'a'
    re.search("a", "cat"),                  # * 'cat' съдържа буквата 'a'
    not re.search("c", "dog"),              # * 'dog' не съдържа буквата 'c'
    3 == len(re.split("[ab]", "carbs")),    # * Разделя думата на три части ['c','r','s'] по 'a' или 'b'
    "R-D-" == re.sub("[0-9]", "-", "R2D2")  # * Заменя цифрите с тирета
    ])                                      # Изход: True

Обектно-ориентирано програмиране Object-Oriented Programming

Както много други езици, Python ви позволява да дефинирате класове, които капсулират данни, и функции, които оперират с тях. Понякога ги използваме, за да направим кода си по-ясен и лаконичен. Вероятно най-лесният начин да ги обясним е чрез изграждането на пример с много коментари. Да предположим, че няма вграден Python сет (Set) и искаме да създадем собствен клас Set. Какви функционалности трябва да има този клас? Например, ако имаме Set, трябва да можем да добавяме елементи към него, да премахваме елементи от него и да проверяваме дали съдържа определена стойност. Затова ще създадем всички тези функционалности като член-функции на класа. Така ще можем да достъпваме тези член-функции чрез точка след обекта Set:

# По конвенция даваме на класовете имена в _PascalCase_
class Set:
    # Това са член-функции
    # Всяка член-функция има параметър "self" на първо място (друга конвенция)
    # "self" съответства на конкретния обект Set, който се използва

    def __init__(self, values=None):
        """Това е функцията за създаване
        Извиква се всеки път, когато създадете нов Set
        Може да се извика така
        s1 = Set() # празен сет
        s2 = Set([1,2,2,3]) # инициализира сет с дадени стойности"""
        self.dict = {} # Всеки екземпляр на Set има собствен атрибут `dict`
        # Използваме този атрибут, за да проследяваме всеки член.
        if values is not None:
            for value in values:
            self.add(value)

    def __repr__(self):
        """Това е строковото представяне на обекта Set
        Можете да го видите, като въведете обекта в командния прозорец на Python или използвате метода str()"""
        return "Set: " + str(self.dict.keys())

    # Ще представяме членството като ключове в self.dict, със стойност True
    def add(self, value):
        self.dict[value] = True

    # Ако параметърът е ключ в речника, съответната стойност е в Set
    def contains(self, value):
        return value in self.dict

    def remove(self, value):
        del self.dict[value]

След това можем да използваме Set по следния начин:

s = Set([1,2,3])
s.add(4)
print s.contains(4)     # True
s.remove(3)
print s.contains(3)     # False

Функционални инструменти Functional Tools

Частични функции partial

Когато предаваме функции, понякога искаме да използваме част от функционалността на дадена функция, за да създадем нова. Като прост пример, да предположим, че имаме функция с две променливи:

def exp(base, power):
    return base ** power

Искаме да я използваме, за да създадем функция, която приема една променлива и връща резултата от степенна функция exp(2, power) с основа 2.

Разбира се, можем да дефинираме нова функция с def, макар и това да не изглежда най-мъдрото решение:

def two_to_the(power):
  return exp(2, power)

По-интелигентният подход е да използваме метода functools.partial:

from functools import partial
two_to_the = partial(exp, 2)      # сега функцията има само една променлива
print two_to_the(3)               # 8

Ако са зададени имена, методът partial може да се използва и за попълване на други параметри:

square_of = partial(exp, power=2)
print square_of(3)                # 9

Ако се опитате да използвате параметри по хаотичен начин в средата на функция, програмата бързо ще стане объркана, затова моля, избягвайте подобно поведение.

Картографиране map

Понякога също така използваме функции като map, reduce и filter като алтернатива на функционалността на списъчните изрази:

def double(x):
    return 2 * x

xs = [1, 2, 3, 4]
twice_xs = [double(x) for x in xs]      # [2, 4, 6, 8]
twice_xs = map(double, xs)              # същото
list_doubler = partial(map, double)     # функцията удвоява списъка
twice_xs = list_doubler(xs)             # също е [2, 4, 6, 8]

Методът map може да се използва и за картографиране на функции с множество параметри към множество списъци:

def multiply(x, y): return x * y

products = map(multiply, [1, 2], [4, 5])  # [1 * 4, 2 * 5] = [4, 10]

Филтриране filter

По същия начин филтърът реализира функционалността на if в списъчните изрази:

def is_even(x):
    """Връща True, ако x е четно, и False, ако x е нечетно"""
    return x % 2 == 0

x_evens = [x for x in xs if is_even(x)]   # [2, 4]
x_evens = filter(is_even, xs)             # същото
list_evener = partial(filter, is_even)    # тази функция филтрира
x_evens = list_evener(xs)                 # също е [2, 4]

Редуциране reduce

Методът reduce непрекъснато обединява първия и втория елемент от списъка, след това обединява резултата с третия елемент и повтаря този процес, докато не се получи единствен резултат:

x_product = reduce(multiply, xs)          # = 1 * 2 * 3 * 4 = 24
list_product = partial(reduce, multiply)  # тази функция редуцира списък
x_product = list_product(xs)              # също е 24

Изброяване enumerate

Понякога се случва да трябва да използваме елемента и неговия индекс едновременно, докато итерираме през списък:

# Не много питонично (не е кратко и елегантно)
for i in range(len(documents)):
    document = documents[i]
    do_something(i, document)

# Също не е много питонично (не е кратко и елегантно)
i = 0
for document in documents:
    do_something(i, document)
    i += 1

Най-краткият начин е да използвате метода за изброяване enumerate, който генерира кортежи (index, element):

for i, document in enumerate(documents):
    do_something(i, document)

По същия начин, ако искате да използвате само индекса:

for i in range(len(documents)): do_something(i)   # не е кратко
for i, _ in enumerate(documents): do_something(i) # кратко

Ще използваме този метод често в бъдеще.

Компресиране и разопаковане на аргументи zip and Argument Unpacking

Компресиране zip

Често обработваме два или повече списъка чрез компресиране (zipping). Компресирането всъщност е преобразуване на няколко списъка в един списък от съответни кортежи:

list1 = ['a', 'b', 'c']
list2 = [1, 2, 3]
zip(list1, list2)       # Получава се [('a', 1), ('b', 2), ('c', 3)]

Разопаковане на аргументи Argument Unpacking

Ако няколко списъка имат различна дължина, процесът на компресиране ще спре в края на най-късия списък. Можете също така да използвате странен трик за декомпресиране (“unzip”) на списъци:

pairs = [('a', 1), ('b', 2), ('c', 3)]
letters, numbers = zip(*pairs)

Звездичката тук се използва за разопаковане на аргументи, като тя използва елементите на pairs като отделни аргументи за zip. Следващият начин на извикване има същия ефект:

zip(('a', 1), ('b', 2), ('c', 3))  # връща [('a','b','c'), ('1','2','3')]

Разопаковането на аргументи може да се използва и с други функции:

def add(a, b): return a + b

add(1, 2)           # Връща 3
add([1, 2])         # Грешка
add(*[1, 2])        # Връща 3

Макар и не толкова практичен, това е добър трик за по-кратък код.

Предаване на променлив брой аргументи args and kwargs

Да предположим, че искаме да създадем функция от по-висок ред, която приема стара функция и връща нова функция, която е старата функция, умножена по 2:

def doubler(f):
    def g(x):
      return 2 * f(x)
    return g

Пример за изпълнение:

def f1(x):
    return x + 1

g = doubler(f1)
print g(3)        # 8 (== ( 3 + 1) * 2)
print g(-1)       # 0 (== (-1 + 1) * 2)

Но ако броят на предаваните аргументи е по-голям от един, този подход не е толкова ефективен:

def f2(x, y):
    return x + y

g = doubler(f2)
print g(1, 2) # Грешка TypeError: g() takes exactly 1 argument (2 given)

Затова трябва да дефинираме функция, която може да приема произволен брой аргументи, и след това да използваме разопаковане на аргументи за предаване на множество такива – което изглежда малко магически:

def magic(*args, **kwargs):
    print "unnamed args:", args
    print "keyword args:", kwargs
magic(1, 2, key="word", key2="word2")
# Резултат:
# unnamed args: (1, 2)
# keyword args: {'key2': 'word2', 'key': 'word'}

Когато дефинираме функция по този начин, args (съкращение от arguments) е кортеж, съдържащ ненаименуваните аргументи, докато kwargs (съкращение от keyword arguments) е речник, съдържащ наименуваните аргументи.

Те могат да се използват и когато предаваните аргументи са списък или кортеж:

def other_way_magic(x, y, z):
    return x + y + z

x_y_list = [1, 2]
z_dict = { "z" : 3 }
print other_way_magic(*x_y_list, **z_dict)    # 6

Можете да го използвате по най-различни необичайни начини, но ние ще го прилагаме само за решаване на проблема с предаването на променлив брой аргументи към функции от по-висок ред:

def doubler_correct(f):
    """Работи ефективно, независимо какво е f"""
    def g(*args, **kwargs):
        """Независимо от броя на параметрите, тази функция ги предава правилно на f"""
        return 2 * f(*args, **kwargs)
    return g

g = doubler_correct(f2)
print g(1, 2) # 6

Добре дошли в света на науката за данни!

Дин! Поздравления, отворихте вратата към един нов свят! Сега можете да се забавлявате и да експериментирате!

Свързано четене:

Често използвана Python синтаксис в науката за данни (основни понятия)