Problem 1. Python / Generator functions

Следующая функция возвращает текущее и предыдущее значения в цикле:


In [3]:
def this_and_prev(iterable):
    iterator = iter(iterable)
    prev_item = None
    curr_item = next(iterator)
    for next_item in iterator:
        yield (prev_item, curr_item)
        prev_item = curr_item
        curr_item = next_item
    yield (prev_item, curr_item)

In [4]:
for i,j in this_and_prev( range(5) ): 
    print(i,j)


None 0
0 1
1 2
2 3
3 4

По аналогии требуется написать функцию, которая будет возвращать текущее и следующее значения.

Type your code below


In [6]:
def this_and_next(iterable): # немного изменил код по сравнению с предыдущей функцией, если следующего значения нет, возвращаем ноль
    iterator = iter(iterable)
    curr_item = next(iterator)
    for next_item in iterator:
        yield (curr_item, next_item)
        curr_item = next_item
    yield (curr_item, None)
    
for i,j in this_and_next( range(5) ): 
    print(i,j)


0 1
1 2
2 3
3 4
4 None

Problem 2. SQL / Python

Есть следующая SQL таблица sample_table:

column namedriver_id start_timestamp status
data type(String)(String)(String)
1driver_id_12017-01-21 00:05driving
2driver_id_12017-01-21 00:09waiting
............
k x ndriver_id_n2017-01-21 23:49transporting
  • driver_id_i -- идентификатор i-го водителя
  • start_timestamp -- время начала статуса, в котором находился водитель
  • status -- статус, в котором находился водитель

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


Табличка хранится в СУБД, которая умеет применять к данным функции, написанные на Python. Например, следующий код выполняет функцию ROW_NUMBER():


In [3]:
def row_number(driver_id, input_data):
    sorted_data = sorted(input_data, lambda x: x[0]) # сортируем список входных данных по дате
    result = []
    row_number = 0
    while row_number <= range( 0, len(input_data) ):
        row_data = {'row_number': row_number
                    , 'driver_id': driver_id
                    , 'start_timestamp': sorted_data[row_number][0]
                    , 'status': sorted_data[row_number][1]
                    }
        row_number += 1
        result.append(row_data)
    return result

In [1]:
$row_number = Python::row_number(driver_id, input_data);

$raw = (
    SELECT 
            driver_id
            , start_timestamp
            , status
    FROM    sample_table
    );

$reduced = (
    REDUCE $raw
       ON  driver_id
    USING  $row_number((start_timestamp, status))
    );

SELECT * FROM $reduced;


  File "<ipython-input-1-819d48a44550>", line 1
    $row_number = Python::row_number(driver_id, input_data);
    ^
SyntaxError: invalid syntax


Результат выполненного запроса будет выглядеть как:

column namerow_numberdriver_id start_timestamp status
data type(Int32)(String)(String)(String)
11driver_id_12017-01-21 00:05driving
22driver_id_12017-01-21 00:09waiting
...............
k x nkdriver_id_n2017-01-21 23:49transporting


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

Type your code below


In [ ]:
def LEAD(driver_id, input_data):
    sorted_data = sorted(input_data, lambda x: x[0]) # сортируем список входных данных по дате
    result = []
    row_number = 0
    while row_number < len(input_data) - 1: # для всех состояний конкретного водителя, кроме финального, добавляем ещё одно значеник
        row_data = {'row_number': row_number
                    , 'driver_id': driver_id
                    , 'start_timestamp': sorted_data[row_number][0]
                    , 'status': sorted_data[row_number][1]
                    , 'status_next': sorted_data[row_number + 1][1]
                    }
        row_number += 1
        result.append(row_data)
    row_data = {'row_number': row_number
                , 'driver_id': driver_id
                , 'start_timestamp': sorted_data[row_number][0]
                , 'status': sorted_data[row_number][1]
                , 'status_next': None # если состояние водителя финальное, то ставим в следующий статус значение None
                }
    result.append(row_data)
    return result

Кроме того, обращаю Ваше внимание на то, что в куске кода "reduce... on... using", возможно, есть ошибка - в аргументах функции row_number, скорее всего, пропущен аргумент driver_id, то есть, по моему мнению, правильным вариантом было бы row_naumber(driver_id, (start_timestamp, status))

Problem 4. SQL

Есть следующая таблица с заказами клиентов:

column nameid client_id driver_id timestamp cost payment_type status
data type(String) (String) (String) (String) (Double) (String) (String)
1some_id some_client_id some_driver_id 2016-01-21 12:03 123.0 cash completed
2some_id some_client_id some_driver_id 2016-01-21 11:42 99.0 card rider_canceled
...... ... ... ... ... ... ...
nsome_id some_client_id some_driver_id 2016-01-21 15:16 0.0 card driver_canceled

Нужно посчитать следующие метрики:

1. Как процент выполнения заказов зависит от типа оплаты?
2. Какой процент активных водителей совершает в неделю более 30 поездок?
3. Какой процент клиентов, совершивших первую поездку за наличные впоследствии переходит на оплату картой?

Type your answer below


In [ ]:
$orders_card = ( # здесь я предполагал, что исходная таблица имеет название sample_table, вытянули оттуда количество всех заказов, оплаченные картой
    SELECT COUNT(*) 
        FROM sample_table
            WHERE payment_type = 'card'
    );

$orders_cash = ( # количество всех заказов, оплаченных наличными
    SELECT COUNT(*) 
        FROM sample_table
            WHERE payment_type = 'cash'
    );

$orders_card_completed = ( # количество всех выполненных заказов, оплаченных картой
    SELECT COUNT(*) 
        FROM sample_table
            WHERE payment_type = 'card'
                AND status = 'completed'
    );

$orders_cash_completed = ( # количество всех выполненных заказов, оплаченных наличными
    SELECT COUNT(*) 
        FROM sample_table
            WHERE payment_type = 'cash'
                AND status = 'completed'
    );

In [ ]:
print(orders_card_completed/orders_card, orders_cash_completed/orders_cash) # посчитали отношения, теперь их нужно сравнить

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


In [ ]:
SELECT # перевели все данные в формат datetime
    CONVERT(DATETIME, CONVERT(VARCHAR(30), timestamp), 120)
FROM sample_table;

$sample_table_completed = ( # взяли из таблицы только выполненные заказы
    SELECT *
        FROM sample_table
            WHERE status = 'completed'
    );

$rides_on_a_week = ( # сгруппировали табличку по водителям
        SELECT driver_id, MIN(timestamp) AS first_trip, MAX(timestamp) AS last_trip, COUNT(id) AS count_trips
            FROM sample_table_completed
        GROUP BY driver_id
    );

SELECT driver_id, count_trips / DATEDIFF(week, first_trip, last_trip) #для каждого водителя посчитали среднее его поездок в неделю
    FROM rides_on_a_week

Я предполагаю, что клиент перешёл на оплату картой, если он первую поездку оплатил наличными, а свою последнюю поездку картой. Все таблицы из предыдущих запросов предполагаются сохранёнными.


In [ ]:
$first_last_trips = ( # для каждого клиента находим дату и время его первой и последней поездки
    SELECT client_id, MIN(timestamp) AS first_trip, MAX(timestamp) AS last_trip
        FROM sample_table_completed
    GROUP BY client_id
);

$joined_table = ( # соединяем таблицы
    SELECT *
        FROM sample_table_completed
    LEFT JOIN first_last_trips
    ON sample_table_completed.client_id = first_last_trips.client_id
);

$clients_paid_first_cash = ( # ищем всех клиентов, которые первую поездку оплатили наличными
    SELECT client_id
        FROM joined_table
    WHERE timestamp = first_trip AND status = 'cash'
);

$clients_paid_first_cash_then_card = ( # ищем всех клиентов, которые первую поездку оплатили картой, а последнюю - наличными
    SELECT client_id
        FROM joined_table
    WHERE timestamp = last_trip AND status = 'card' AND (client_id IN clients_paid_first_cash)
);

$share_of_clients = ( # считаем долю
    SELECT (COUNT(*) 
            FROM clients_paid_first_cash_then_card) / (COUNT(*) FROM clients_paid_first_cash)
);

Problem 5. Algorithms

Город порезан на "квадраты". В момент t возникает точка на карте в пределах выделенной зоны (большого квадрата). Определить в какой из малых квадратов она попадет можно, например, с помощью перебора (bruteforce), который в среднем будет решать задачу за линейное время. Какой более эффективный алгоритм можно предложить для решения данной задачи и за какое время он будет ее решать?

Для выполнения задания не требуется писать код, можно описать логику алгоритма в 5-10 предложениях.

Type your answer below

В данной задаче я бы применил бинарный поиск по двум координатам. Мы делим большой квадрат на 4 квадрата одинакового размера (длину пополам и ширину пополам) и смотрим по двум координатам, куда именно попала наша точка (сравниваем первую координату с координатой середины по ширине и вторую координату с координатой середины по длине). Затем берем тот квадрат из четырех, в который попадает наша точка, и проделываем подобную операцию с ним же и так, пока не дойдем до того квадрата, в котором и лежит наша точка. Если считать, что ширина и длина в маленьких квадратах порядка $n$, то сложность алгоритма $O(\log{n^2}) = O(\log{n})$.

Problem 6. A/B Testing

Необходимо понять, как прохождение обучения работе с приложением влияет на конверсию водителей из заявки на сайте (лид) в первую поездку (начало работы). Среди 1200 лидов прошедших обучение первую поездку сделали 370, среди группы не прошедшей обучение поехали 1250 из 4500 водителей. Какое решение вы бы приняли и почему?

Допустим, эксперимент показал, что конверсия выросла. Рассматривается возможность сделать обучение обязательным. Как это повлияет на показатели привлечения? Можно ли принимать это решение основываясь только на конверсии?

Следующий шаг эксперимента - в дополнение к конверсии нужно сравнить выручку, которую приносит водитель за первый месяц работы. Как правильно рассчитать эту выручку? Допустим, в группе с обучением, средняя выручка составила 52к рублей, а в группе без обучения 49к. Как бы вы принимали это решение основываясь на выручке и конверсии? Какой KPI на ваш взгляд важнее и почему? Меняется ли что-то в статистическом подходе к сравнению при переходе от конверсии к выручке?

Type your answer below

Предположим, что конверсия конкретного водителя из заявки на сайте в первую поездку описывается бернуллиевской случайной величиной. Оценим $\hat p$, если водитель прошел обучение. $\hat p = 0,308333$, если водитель не прошел обучение, $\hat p = 0,277777778$, оцениваемая дисперсия случайных величин будет равна $\sigma^2_1 = ((n-1)\hat p \hat q)/n = 0,213086$ в первом случае, и $\sigma^2_2 = 0,20057$ во втором. Нам нужно оценить, есть ли влияние работы с приложением на конверсию, для этого необходимо проверить гипотезу, является ли разность доли сделавших первую поездку в первой группе и во второй случайной величиной с нулевым матожиданием (здесь применили центральную предельную теорему, считая, что все водители принимают решение независимо и одинаково распределенно) и стандартным отклонением, равным сумме стандартных отклонений доль сделавших первую поездку в первой и во второй группах. Это стандартное отклонение равно $\sqrt{\sigma^2_1/n_1 + \sigma^2_2/n_2} = 0,0149$, где $n_1, n_2$ - количества водителей в первой и второй группах. Разность доль водителей, сделавших первую поездку в первой группе и во второй, равна $0,03056$. С учетом того, что квантиль стандртного нормального распределения 97,5% равен 1,96, то получаем, так как $0,0149\cdot1,96 < 0,03056$, то гипотеза о том, что прохождение обучения работе с приложением не влияет на конверсию, отвергается, это значит, что конверсия выросла, поэтому стоит рассмотреть возможность сделать обучение обязательным.

Вероятно, многие водители не захотят проходить обучение работе с приложением (добровольно, насколько я понимаю из имеющихся данных, работу выбрали всего 1200 водителей из 5700), поэтому они откажутся работать с Яндекс.Такси, поэтому показатели привлечения, видимо, упадут. Поэтому принимать это решение, основываясь только на конверсии, нельзя - возможно, мы потеряем много водителей, которые станут работать с нашими конкурентами, такими как Убер и Гетт, а также это может привести к ухудшению качества работы сервиса, то есть увеличению задержек из-за недостатка водителей.

Как мне кажется, стоит рассчитывать ту выручку, которая идет напрямую Яндекс.Такси, потому что процент от поездки, который идет в Яндекс.Такси, зависит от дальности поездки - чем дальше поездка, тем больше процент. Далее, я бы проверил гипотезу о том, что прохождение обучения не влияет на выручку водителя (аналогично пункту про конверсию, только тут необходима ещё оценённая дисперсия). Если опять же, выяснилось бы, что гипотеза отвергается, то это значит, что стоит рассмотреть возможность сделать обучение обязательным. Если же нет, то как мне кажется, более важный KPI - это конверсия, так как среднюю выручку можно увеличить в последующие месяцы, но только если водитель сделал первую поездку и стал принимать заказы от Яндекс.Такси.

Problem 7. Efficiency

В часы пик количество желающих воспользоваться такси резко возрастает и машин начинает не хватать. Для того чтобы обеспечить надежность сервиса в платформу заложен механизм балансировки спроса и предложения через динамическое ценообразование (surge pricing).

  1. От чего должен зависеть повышающий коэффициент (surge) и почему? Предложите алгоритм управления surge коэффициентом.
  2. Какие граничные условия вы бы предложили в качестве целевых (если коэффициент слишком низкий многие люди не могут уехать; если коэффициент слишком высокий - никто не хочет ехать)?
  3. Какие метрики нужно отслеживать чтобы понять, что алгоритм А работает лучше чем алгоритм Б?

Type your answer below

  1. Повышающий коэффициент должен зависеть от количества заказов и количества водителей в данном районе (городе), от цен конкурирующих онлайн-агрегаторов такси (Убер, Гетт) (если у нас цены слишком высокие, то клиенты могут перейти к конкурентам вне зависимости от их времени подачи), от средней доли отмененных заказов (как по вине водителя, так и по вине клиента) (понятно, что нас интересуют только выполненные заказы), от эластичности спроса по цене (если есть соответствующие исследования, нужно понимать, скольких клиентов мы лишимся, увеличив цену на поездку в среднем на один рубль), от себестоимости возможной поездки на личном автомобиле для клиента, от оборачиваемости машин (то есть среднего времени одной поездки, понятно, что чем меньше по времени средний заказ, тем больше заказов может обслужить одна машина в час пик). Самый лучший способ управления коэффициентом - это его динамическое изменение, исходя из параметров, описанных выше (нужно построить какую-то модель, возможно, модель линейной регрессии, для автоматического нахождения нужного повышающего коэффициента в данном районе в конкретный момент времени).

  2. Граничные условия здесь определяются так: а) граничное ограничение на коэффициент снизу - он должен быть не меньше, чем коэффициент, при котором достигается полная загрузка имеющихся водителей и машин (цены ниже нет смысла ставить, потому что всех желающих все равно не сможем увезти, но получим меньше денег); б) граничное ограничение на коэффициент сверху - мы не должны терять определённой доли клиентов среди тех, кого мы можем отвезти имеющимися машинами - иначе они могут перейти на сервисы конкурентов или вовсе не пользоваться онлайн-такси - как мне кажется, наибольший возможный коэффициент - коэффициент, при котором перевозится 80-90% от всех клиентов, которые могут быть перевезены имеющимися машинами.

  3. Ключевая метрика - выручка Яндекс.Такси в конкретный день в час пик (можно сравнивать, например, одни и те же промежутки времени в один и тот же рабочий день), кроме того, важны показатели удовлетворения клиентов (доля отвезенных клиентов к числу заказов) (мы должны перевозить, как сказано в предыдущем пункте, почти всех возможных клиентов в этот день), показатели удовлетворения водителей (средний простой водителя в час пик) (естественно предположить, что при нашем повышающем коэффициенте в час пик какие-то водители все равно могут оказаться невостребованными), среднее время подачи машины.