Предисловие

Многие из программистов хотят выучить английский язык по ряду причин. Кто то посещает курсы в оффлайне, занимается с частным репетитором в свободное от работы время. Другие же предпочитают занятия в онлайн-режиме, без траты времени на дорогу. Да и найти подходящий вариант в этом случае не так уж сложно - поскольку сейчас рынок онлайн-образования растет весьма быстро и предоставляет варианты подходящие под Ваш вкус и кошелёк. Весьма вероятно что читатель знает о компаниях которые предлагают свои услуги в этом сегменте. Если вам подходят любой из вышеупомянутых вариантов - можете не читать дальше, иным же, знакомым с Python/Pandas/Seaborn и прочие радостями анализа и визуализации данных - добро пожаловать.

Предупреждение: Действия описываемые автором сработали в его ситуации, и могут не сработать в вашей ситуации. Хотя,кто знает…

Введение

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

Но вот тут то и таится главная неприятность - цена за услуги такого преподавателя. Средняя цена для сервисов которые предоставляют услуги в российском сегменте интернета - 1200 рублей за один час занятий с носителем языка. Зачастую это академический час(45 минут).
Для справки - курс на момент написания данной статьи - 57.32р за 1$, получается немногим более 20$.

Конечно, все познается в сравнении. И сравнивая с одним из альтернативных вариантов - за 20$ можно найти больше интересных предложений и даже попробовать некоторые из них за символическую сумму. Говоря это я подразумеваю сервис Italki (моя реферальная ссылка) , описание которого не буду приводить тут, поскольку в Сети достаточно мест где это уже описали.

Интерфейс сервиса незамысловат, вы можете настроить фильтрацию преподавателей по нужным вам критериям и в итоге получить выборку подходящую под ваши запросы. Точнее оно должно бы так работать… но не работает, а иногда возвращает совсем не то что ожидалось, лишний мусор который не попадает под критерии фильтрации.

Как пример одной из наиболее неприятных проблем, можно привести невозможность установки максимальной цены которую вы готовы платить за занятия. Я не был готов платить за урок более 15$, но предустановленные фильтры дают возможность выбрать только из ранее заданных сегментов, например 10-20$ в результате чего получалось много “мусорных” записей.
title неоптимальный поиск

Впрочем у сервиса есть API, так что эту проблему можно обойти, что и будет описано ниже.
На данном моменте у нас есть следующие критерии:


  • Платить не более 15$

  • Профессиональный преподаватель

Шаг 1. Получение данных.

Как уже было упомянуто - фильтрация по цене за час очень проблематична. Поэтому решено было зайти с другой стороны и выгрузить с сервиса все данные по некоторым англоязычным странам, откуда родом преподаватель. Начнём наше путешествие.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import json
from random import randint
from time import sleep
import requests
#Step 1
#Основой URL для API-запросов
url = 'https://www.italki.com/api/teachersv2'
#Коды стран, преподавателей из которых рассматриваем
country_codes = ['GB', "US", "AU", "CA", "IE", "NZ"]
#временное хранилище данных
first_step = 'step_1.json'
#Изучаемый язык
lang = 'english'
#Параметры запроса.
#page_size - Количество записей в ответе.
#teacher_type.1-Профессиональный преподаватель, 0-Неформальное общение
#is_native - является ли преподаватель носителем языка.1-да,0-нет.
#price_usd - сколько готовы заплатить за урок.Внезапно, но сумма в центах.
params = {'teach': lang,
'speak': lang,
'page_size': 50,
'teacher_type': 1,
'is_native': 1,
'price_usd': 'min-1500'}
teachers = []
for code in country_codes:
has_next = True
params['country'] = code
params['page'] = 1
while has_next:
req = requests.get(url, params=params)
response = req.json()
data = response['data']
teachers.extend(data)
params['page'] += 1
has_next = response['meta']['has_next']
sleep(randint(2, 5))
with open(first_step, 'w') as outfile:
json.dump(teachers, outfile)

Модель получения данных очень проста - запрашиваем API с определёнными параметрами, получаем данные, сохраняем в JSON-файл.На данном этапе у нас даже нет понятия о реальных ценах за уроки, всё что мы знаем - какова минимальная и максимальная цена за их уроки. Последние два момента, связанные с ценами - совершенно бесполезная информация, поскольку преподаватели могут вести занятия на более чем одном языке и к примеру цена за английский язык может быть 10$, а максимальная - 11$, потому что преподаватель еще и преподает китайский.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
second_step = 'step_2.json'
country_names = ['Great Britain', "USA", "Australia", "Canada", "Ireland", "New Zealand"]
countries = dict(zip(country_codes, country_names))
with open(first_step) as json_data:
records = json.loads(json_data.read())
for record in records:
code = record['origin_country_id']
record['country'] = countries[code]
df = pd.DataFrame.from_records(records)
df.to_json(second_step, date_format='iso', orient='records')
sns.countplot(x="country", data=df)
plt.show()

На данной стадии все что мы можем увидеть - количество преподавателей с привязкой к странам. Рынок данного сегмента можно выразить следующим образом

title Общее представление

Получилось более 350 предложений, которые нужно обогатить дополнительной информацией чтоб получить уже более реальные цены.

Шаг 2. Уточнение данных.

На этом шаге мы получим дополнительную информацию,что позволит нам отфильтровать предложения и найти наиболее подходящие под наши требования.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
limit = 15
third_step = 'step_3.json'
#Открываем сохраненные данные с прошлого шага
with open(second_step) as json_data:
records = json.loads(json_data.read())
teachers.clear()
hour_lesson_id = 4 # идентификатор для урока длиной в 60 минут
for idx, record in enumerate(records):
record['actions'] = ''
#Цены за уроки
prices = []
req = requests.get('https://www.italki.com/api/teacher/{}'.format(record['id']))
response = req.json()
data = response['data']
#Информация о уроках которые готов предоставить преподаватель
classes = data['teacher_course_obj_s']
#Отбираем только уроки по английскому
classes = filter(lambda x: (x['language'] == lang), classes)
for c in classes:
# Описание услуги. Например "English Grammar".
action = c['title'].lower()
if action:
lesson_price = c['session_price_usd'] / 100
if lesson_price <= limit:
#Урок в рамках той суммы что мы готовы платить
record['actions'] = record['actions'] + " " + action
prices.append(lesson_price)
if prices:
record['price'] = median(prices)
teachers.append(record)
sleep(randint(2, 4))

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

1
2
3
4
df = pd.DataFrame.from_records(teachers)
df.to_json(third_step, date_format='iso', orient='records')
sns.swarmplot(y="price", x="country", data=df)
plt.show()

title Общее представление

Данная картина уже более информативна, но всё же просмотреть глазами все эти варианты - достаточно проблематично, поэтому используем функции Pandas для фильтрации по некоторым ключевым словам, о которых будет сказано позже

Шаг 3. Фильтрация данных

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

Тут можно использовать ключевые слова которые встречаются в описании профиля.
Таким словами стали teacher (что подразумевает что человек уже имел опыт преподавания какое то время) и been teaching что намекает что человек в текщий момент практикующий преподаватель.
Также отделим преподавателей услугами которых воспользовались более чем 10 студентов.
И на финальном этапе фильтрации отделим преподавателей которые специализируются на том что интересно именно конкретному человеку - в моём случае - это грамматика английского языка - критерий grammar

В результате список требований немного изменился и имеет вид


  • У преподавателя занималось более 10 студентов

  • Профессиональный преподаватель

  • Специализируется на грамматике языка

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import re
forth_step = 'step_4.json'
with open(third_step, 'r') as json_data:
records = json.load(json_data)
for record in records:
record['intro'] = record['teacher_info_obj']['intro']
record['student_count'] = record['teacher_info_obj']['student_count']
df = pd.DataFrame.from_records(records)
df = df[df.intro.str.contains("teacher", flags=re.IGNORECASE) | df.intro.str.contains("been teaching")]
df = df[df.actions.str.contains("Grammar", flags=re.IGNORECASE)]
df = df[df.student_count > 10]
df.to_json(forth_step, date_format='iso', orient='records')
sns.swarmplot(y="price", x="country", data=df)
plt.show()

title Отфильтрованные данные
Данный подход позволил отсеять большее количество преподавателей и их осталось совсем немного.

Шаг 4.Финальный.


После того как число претендентов изрядно уменьшилось - осталось только визуализировать полученные данные и дать необходимую информацию об потенциальных кандидатах звание “Мой преподаватель английского”.

Немного развернём данные и добавим мета-информацию по ним.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from collections import defaultdict
with open(forth_step) as json_data:
records = json.loads(json_data.read())
df = pd.DataFrame.from_records(records)
ax = sns.swarmplot(x="price", y="country", data=df)
#Пометки на графике
countries_labels_position = [item.get_text() for item in ax.get_yticklabels()]
idf = df.set_index("country")
for country in set(df.country):
country_row = countries_labels_position.index(country)
rows = idf.loc[country]
prices = rows.price.values.tolist()
nicknames = rows.nickname.values.tolist()
pr2nick = defaultdict(list)
for price, nick in zip(prices, nicknames):
pr2nick[price].append(nick)
for price,nicknames in pr2nick.items():
nicknames = '\n'.join(nicknames)
ax.annotate(nicknames, (price+0.01, country_row+0.05),size=9)
plt.show()

Дало следующую картину
title Отфильтрованные данные

А отладочный вывод

1
2
3
4
5
6
7
8
9
10
11
12
import pprint
#Отладочная печать для получения id профилей преподавателя
country2price = defaultdict(dict)
for country, value, price in zip(df.country, df.id, df.price):
if not (price in country2price[country]):
country2price[country][price] = []
country2price[country][price].append(value)
pp = pprint.PrettyPrinter(indent=4)
pp.pprint(country2price)

Позволил получить идентификаторы преподавателей среди которых не составило большого труда найти наиболее подходящего, используя id для получения ссылки на профиль.

Итого

Данные действия позволили найти очень хорошего преподавателя, в моём случае это преподаватель из Канады за 14$, что несомненно более выгодно чем тратить свыше 20$ и при этом иметь сравнительно небольшой выбор. И если вы до сих пор не пробовали данный сервис - можете начать прям сейчас и получить 10$ на первый урок по их промо-акции

Пару слов о статистическом анализе на этих данных.

В данном случае он не дал ничего, никакой скрытой корреляции не выявлено. Не обнаружено связи между ценой за урок, количеством студентов у преподавателя, количеством оспоренных уроков, рейтингом преподавателя.
title Корреляция Спирмэна