Программирование на языке Python для сбора и анализа данных

Текст лекции: Щуров И.В., НИУ ВШЭ

Данный notebook является конспектом лекции по курсу «Программирование на языке Python для сбора и анализа данных» (НИУ ВШЭ, 2015-16). Он распространяется на условиях лицензии Creative Commons Attribution-Share Alike 4.0. При использовании обязательно упоминание автора курса и аффилиации. При наличии технической возможности необходимо также указать активную гиперссылку на страницу курса. Фрагменты кода, включенные в этот notebook, публикуются как общественное достояние.

Другие материалы курса, включая конспекты и видеозаписи лекций, а также наборы задач, можно найти на странице курса.

Лекция №6. Сортировка. Форматирование строк

Сегодняшняя лекция посвящена нескольким несвязанным сюжетам, относящимся к базовому синтаксису Python, которым не нашлось места в предыдущих лекциях.

**kwargs

Расскажем об одном способе передачи аргументов функции, использующем словари. Напомним два способа передачи аргументов:

In [88]:
def myfunc(x=0, y=1):
    print("x =",x)
    print("y =",y)
In [89]:
myfunc(12,19)
x = 12
y = 19
In [91]:
myfunc(y=2, x=5)
x = 5
y = 2

Как показывает второй пример, аргументы можно передавать, указывая их имена. Допустим, мы хотим написать функцию, которая принимает неопределенное количество именованных аргументов (мы даже не знаем заранее, каких). Это можно сделать следующим образом:

In [92]:
def new_func(**kwargs):
    print(kwargs)
In [93]:
new_func(x=1, y=5, z=8, s = "Some string")
{'y': 5, 'x': 1, 'z': 8, 's': 'Some string'}

Две звёздочки в определении функции new_func() говорят следующее: «все именованные параметры, переданные этой функции, следует поместить в словарь kwargs». Как видим, именно так это и работает: например, параметр x=1 превратился в запись 'x': 1 в словаре kwargs. Однако, это происходит только с «бесхозными» параметрами: если бы у функции был отдельный параметр x, то он бы не попал в kwargs. Например:

In [94]:
def other_func(x, **kwargs):
    print(kwargs)
other_func(x=10,y=5)
{'y': 5}

Сортировка

Сортировка — то есть расположение элементов списка в каком-то определённом порядке — распространённая программистская задача. Мы не будем здесь обсуждать алгоритмы сортировки — благо, они уже все давно реализованы и вам почти наверняка не придётся реализовывать свой (хотя, конечно, разобраться с тем, как устроены quicksort и mergesort, и реализовать их самостоятельно — это полезное и приятное упражнение).

Для сортировки списков в Python есть два главных инструмента. Первый — метод sort(), выполняющий сортировку in place, то есть внутри самого списка. Например:

In [95]:
my_list = [6, 9, 2, 7, 12, 8]
In [96]:
my_list.sort()
In [97]:
my_list
Out[97]:
[2, 6, 7, 8, 9, 12]

Метод sort() меняет исходный список (и поэтому, кстати, умеет работать только со списками — у кортежей такого метода нет). Если вместо этого вы хотите создать новый список, то следует использовать функцию sorted().

In [98]:
my_list = [6, 9, 2, 7, 12, 8]
In [101]:
sorted_list = sorted(my_list)
sorted_list
Out[101]:
[2, 6, 7, 8, 9, 12]

Так мы создали новый список. Старый при этом остался без изменений.

In [102]:
my_list
Out[102]:
[6, 9, 2, 7, 12, 8]

Функцию sorted() можно применять не только к спискам, но и к неизменяемым последовательностям — например, к кортежам. На выходе всегда получается список.

In [104]:
my_tuple = (7, 1, 2, 6)
print (sorted(my_tuple))
[1, 2, 6, 7]

Сортировка строк

Сортировать можно списки, состоящие не только из чисел, но и из более сложных объектов — лишь бы умели сравнивать их между собой. Например, строки можно сравнивать между собой — они упорядочены в лексикографическом порядке, то есть «по алфавиту» и так, как они шли бы в словаре (имеется в виду обычный бумажный словарь, а не тип данных Python).

In [107]:
"abcd" < "b"
Out[107]:
True
In [108]:
"abcd" < "addd" # Первая буква совпадает, сравниваем вторую
Out[108]:
True
In [109]:
"a" < "aa"
Out[109]:
True

Вот так выглядит сортировка списка из строк:

In [110]:
str_list = ["Bob", "Alice", "Bill", "Weigu"]
str_list.sort()
str_list
Out[110]:
['Alice', 'Bill', 'Bob', 'Weigu']

Сортировка и циклы

Можно использовать функцию sorted() совместно с оператором for чтобы обрабатывать элементы списка в каком-то определённом порядке. Например, у нас есть словарь и мы хотим вывести его элементы в порядке возрастания значения ключа. Тогда об этом надо явно попросить Python, как показывает следующий пример:

In [113]:
gradebook = {'Bob': 3,  'Alice': 5, 'Weigu': 4, 'Bill': 2}
for k in gradebook:
    print(k, gradebook[k])
Bill 2
Alice 5
Bob 3
Weigu 4

Как видите, элементы не упорядочены. Вот так их можно упорядочить при выводе:

In [114]:
for k in sorted(gradebook):
    print(k, gradebook[k]) 
Alice 5
Bill 2
Bob 3
Weigu 4

Более сложные примеры сортировки

Можно сортировать список в обратном порядке (по убыванию). Для этого используется параметр reverse.

In [115]:
sorted([4, 8, 1, 7], reverse=True)
Out[115]:
[8, 7, 4, 1]

Можно сортировать не только числа и строки, но и более сложные объекты. Нарпимер, рассмотрим такую табличку (реализованную как список кортежей), в которой записаны имена студентов и их оценки по нескольким работам.

In [119]:
names = [("Bob", 8, 4, 9),
         ("Alice", 7, 8, 9), 
         ("Weigu", 7, 5, 3), 
         ("Dan", 6, 4, 3)]
In [120]:
names.sort()
In [121]:
names
Out[121]:
[('Alice', 7, 8, 9), ('Bob', 8, 4, 9), ('Dan', 6, 4, 3), ('Weigu', 7, 5, 3)]

Судя по результату, логично предположить, что сортировка была выполнена по первому элементу — имени студента. Действительно, кортежи сравниваются примерно так же, как строки, лексикографически. Сначала сравниваются первые элементы:

In [124]:
('a', 8) < ('b', 7)
Out[124]:
True

Если первый элемент совпадает, то сравниваются вторые элементы и т.д.

In [125]:
('a', 8) < ('a', 7)
Out[125]:
False

А что делать, если бы нам захотелось отсортировать кортежи в списке names не по первому элементу, а по второму или ещё какому-нибудь? Для этого необходимо использовать параметр key, задающий ключ сортировки. Прежде, чем мы это сделаем, нужно сказать несколько слов о том, как можно одной функции передать другую функцию в качестве параметра.

Отступление: функции как аргументы функций

Рассмотрим такую функцию:

In [126]:
def superfunc(f):
    return f(2)

В качестве аргумента она принимает какую-то функцию f, вызывает эту функцию передаёт ей в качестве аргумента число 2 и возвращает тот результат, который вернула f.

Например:

In [129]:
from math import sqrt
superfunc(sqrt)
Out[129]:
1.4142135623730951

Что здесь произошло? Мы импортировали функцию sqrt() из модуля math, после чего передали функции superfunc() функцию sqrt в качестве параметра. Обратите внимание: при передаче после функции sqrt нет скобок: это потому, что мы её не вызываем, а передаём другой функции. Функция superfunc взяла нашу функцию sqrt и вызвала её, передав ей число 2 в качестве параметра. То есть вычислила корень из двух.

Можете представить себе, что sqrt — это рецепт, записанный на бумажке. Мы передаём его в виде такой бумажки функции superfunc и она его как-то использует. Передадим другую бумажку — она использует её. Например:

In [133]:
def plusodin(x):
    return x + 1
In [134]:
superfunc(plusodin)
Out[134]:
3

Если мы попробуем передать функции superfunc что-то другое — например, строчку или число — ничего не получится (она ожидает именно функцию).

In [135]:
superfunc("sqrt")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-135-1d3f3cbd81ed> in <module>()
----> 1 superfunc("sqrt")

<ipython-input-126-621fdda2e116> in superfunc(f)
      1 def superfunc(f):
----> 2     return f(2)

TypeError: 'str' object is not callable

Ключи сортировки

Вернёмся к задаче о сортировке таблицы, представленной в виде списка кортежей. Чтобы отсортировать такой список по второму элементу, необходимо сначала создать функцию, которая будет возвращать второй элемент переданного ей кортежа (или списка).

In [136]:
def get_second_element(x):
    return x[1]

Посмотрим, как она работает:

In [137]:
get_second_element([7, 8, 4, 2])
Out[137]:
8

Теперь мы передаем эту функцию в качестве параметра key методу sort() (функция sorted() тоже сработает):

In [138]:
names.sort(key=get_second_element)
In [139]:
names
Out[139]:
[('Dan', 6, 4, 3), ('Alice', 7, 8, 9), ('Weigu', 7, 5, 3), ('Bob', 8, 4, 9)]

Видно, что теперь строки оказались упорядочены по второму столбцу (первой оценке): у Dan она самая низкая (6), у Bob самая высокая (8), а у Alice и Weigu одинаковые (7).

Возникает естественный вопрос: а как упорядочены в этом случае строчки, соответствующие Alice и Weigu? Ответ: в том порядке, в котором они шли в исходном списке. Это бывает удобно, если вы хотите упорядочить сначала по одному параметру, а потом по другому: просто выполните сортировку последовательно, сначала по второму параметру, а потом по первому.

Чтобы не определять каждый раз функцию типа get_second_element можно использовать готовую: для этого надо импортировать специальную функцию itemgetter:

In [140]:
from operator import itemgetter
In [143]:
sorted(names, key=itemgetter(2))
# упорядочили по третьему столбцу
Out[143]:
[('Dan', 6, 4, 3), ('Bob', 8, 4, 9), ('Weigu', 7, 5, 3), ('Alice', 7, 8, 9)]
In [149]:
sorted(names, key=itemgetter(3))
# упорядочили по четвертому столбцу
Out[149]:
[('Dan', 6, 4, 3), ('Weigu', 7, 5, 3), ('Bob', 8, 4, 9), ('Alice', 7, 8, 9)]

Допустим, мы хотим упорядочить по третьему столбцу, а если третий столбец даёт одну и ту же оценку, то по алфавиту. Это можно сделать так: сначала упорядочим по алфавиту, а потом — по третьему столбцу.

In [150]:
print(names)
names.sort(key=itemgetter(0))
print(names)
names.sort(key=itemgetter(2))
print(names)
[('Bob', 8, 4, 9), ('Dan', 6, 4, 3), ('Weigu', 7, 5, 3), ('Alice', 7, 8, 9)]
[('Alice', 7, 8, 9), ('Bob', 8, 4, 9), ('Dan', 6, 4, 3), ('Weigu', 7, 5, 3)]
[('Bob', 8, 4, 9), ('Dan', 6, 4, 3), ('Weigu', 7, 5, 3), ('Alice', 7, 8, 9)]

Больше подробностей о сортировке можно найти в официальном tutorial, а мы перейдём к следующей теме.

Форматирование строк

Зачастую требуется вставить значение каких-то переменных в какую-то строчку. Пример, который нам уже встречался.

In [151]:
name = "Alice"
grade = 5
print("Student", name,"has grade", grade)
Student Alice has grade 5

С помощью print() можно вывести такую строку на печать, но если бы мы хотели передать её какой-то другой функции, то надо было бы придумывать что-то другое. И это другое уже придумано!

Есть два распространённых способа подставлять значение переменных в строку (это часто называется интерполяцией, хотя не имеет никакого отношения к одноименной математической операции). Первый способ более классический.

In [152]:
new_str = "Student %s has grade %i" % (name, grade)
print(new_str)
Student Alice has grade 5

Здесь используется оператор %, который для строк делает следующую операцию: берёт строку слева от него, находит там все «поля для подстановки» (placeholders) — в данном случае это %s и %i, после чего берёт переменные, перечисленные справа от него (это может быть одна переменная или кортеж из нескольких переменных, как в данном случае) и подставляет их последовательно — первую переменную на место первого placeholder, вторую на место второго и т.д.

Буквы в обозначениях placeholders означают тип переменной: в данном случае %s — это строка, а %i — целое число. Вот ещё несколько примеров:

In [172]:
print("The number is %i" % 2.3)
print("The number is %f" % 2) # f значит float
print("The number is %.2f" % 2.1393) # два знака после точки
print("The number is %04i" % 3) # дополнить до четырёх знаков нулями
The number is 2
The number is 2.000000
The number is 2.14
The number is 0003

При использовании оператор % нужно быть осторожным: он имеет приоритет по сравнению с арифметическими операциями, поэтому вы можете получить неожиданный результат, если не поставите скобки:

In [173]:
print("a = %i" % 3*3)
a = 3a = 3a = 3

Здесь произошло следующее: сначала выполнился код "a = %i" % 3, а потом результат умножился на 3 (что для строчек эквивалентно трёхкратному повторению). Если вы хотели подставить результат выполнения 3*3, то нужно было сделать вот так:

In [174]:
print("a = %i" % (3*3))
a = 9

Второй способ форматирования («новый») заключается в использовании метода format(). Он действует примерно так:

In [175]:
"hello, {0}, this is {1}, again {0}, {var}".format(7, 9, var="test")
Out[175]:
'hello, 7, this is 9, again 7, test'

Здесь не приходится явно указывать типы данных (подставляется строковое представление переменной). Одно и то же значение может использоваться несколько раз (к ним можно обращаться по номерам и именам. Впрочем, можно и не указывать явно номера — тогда переменые будут подставляться по очереди:

In [177]:
"Fist var: {}, the second one: {}".format(8, 1)
Out[177]:
'Fist var: 8, the second one: 1'

Форматирование может быть довольно сложным и никто не в силах запомнить все тонкости. Неплохая документация на эту тему (как по оператору %, так и по методу format()) собрана здесь.

Хитрости с вещественными числами

Кстати:

In [180]:
print("%f" % (0.1+0.2))
0.300000

Вроде бы, ничео неожиданно, но давайте увеличим точность…

In [194]:
print("%.18f" % (0.1+0.2))
0.300000000000000044

Когда мы попросили вывести результат с точностью 18 знаков после запятой, откуда-то взялись непонятные значащие цифры в конце. Это связано с тем фактом, что компьютеры используют двоичную систему счисления, а в ней числа типа 0.1 записываются в виде бесконечной периодической дроби и не могут быть представлены в виде конечной дроби. При арифметических операциях возникают ошибки округления, которые и приводят к таким эффектам.

Иногда эти эффекты становятся опасными. Вы думаете, что 0.1 + 0.2 это 0.3? У вашего компьютера другое мнение на этот счёт:

In [186]:
0.1 + 0.2 == 0.3
Out[186]:
False

Однако, не стоит отчаиваться: вы можете использовать обыкновенные дроби или специальный модуль decimal для работы с десятичными дробями.

In [72]:
from fractions import Fraction
In [192]:
Fraction(1, 10) + Fraction(2, 10)
Out[192]:
Fraction(3, 10)
In [193]:
Fraction(1, 3) + Fraction(1,2)
Out[193]:
Fraction(5, 6)
In [188]:
from decimal import Decimal
In [190]:
Decimal("0.1")+Decimal("0.2")
Out[190]:
Decimal('0.3')
In [191]:
Decimal("0.1") + Decimal("0.2") == Decimal("0.3")
Out[191]:
True

Здравый смысл восторжествовал! Он всегда торжествует. Рано или поздно.

Подробнее о десятичных и бинарных дробях можно прочитать в официальной документации. А мы на этом на сегодня закончим.