<jupyter_start><jupyter_text>Определение перспективного тарифа для телеком компании Вы аналитик компании «Мегалайн» — федерального оператора сотовой связи. Клиентам предлагают два тарифных плана: «Смарт» и «Ультра».**Тариф «Смарт»**1. Ежемесячная плата: 550 рублей2. Включено 500 минут разговора, 50 сообщений и 15 Гб интернет-трафика3. Стоимость услуг сверх тарифного пакета:    - минута разговора: 3 рубля    - сообщение: 3 рубля    - 1 Гб интернет-трафика: 200 рублей**Тариф «Ультра»**1. Ежемесячная плата: 1950 рублей2. Включено 3000 минут разговора, 1000 сообщений и 30 Гб интернет-трафика3. Стоимость услуг сверх тарифного пакета:    - минута разговора: 1 рубль    - сообщение: 1 рубль    - 1 Гб интернет-трафика: 150 рублейЧтобы скорректировать рекламный бюджет, коммерческий департамент хочет понять, **какой тариф приносит больше денег**.В вашем распоряжении данные 500 пользователей «Мегалайна»: кто они, откуда, каким тарифом пользуются, сколько звонков и сообщений каждый отправил за 2018 год.🎯 Нужно проанализировать поведение клиентов и сделать вывод — **какой тариф лучше**.  Changelog- При слиянии датафреймов используется параметр `how='outer'` вместо `right` для того, чтобы получить записи по ключам из обоих датасетов;- Дисперсия и другие метрики считаются не по агрегированным данным. Описание шага 3 ввело в заблуждение;- Скорректированы выводы.  Шаг 1. Откроем файлы с данными и изучим общую информацию<jupyter_code>import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats as st
calls = pd.read_csv('calls.csv')
internet = pd.read_csv('internet.csv')
messages = pd.read_csv('messages.csv')
tariffs = pd.read_csv('tariffs.csv')
users = pd.read_csv('users.csv')<jupyter_output><empty_output><jupyter_text>Для начала рассмотрим каждый из датасетов по отдельности. При необходимости будем соединять данные.  Тарифы<jupyter_code>tariffs.info()
'''
    Сразу же переименуем колонку с названием тарифа, чтобы в дальнейшем у нас была возможность применить merge по ней.
    Также исправим ошибку в названии признака объёма интернет-трафика.
'''
tariffs = tariffs.rename(columns={'tariff_name': 'tariff', 'mg_per_month_included': 'mb_per_month_included'})
tariffs<jupyter_output><empty_output><jupyter_text>С типами данных все ок.📝 Объём интернет-трафика, включённого в абонентскую плату, указан в мегабайтах, а стоимость дополнительного объема интернет-трафика в рублях за гигабайт.  Пользователи<jupyter_code>users.info()
users.head()
users['tariff'].value_counts()
users.isna().sum()
users.duplicated().sum()<jupyter_output><empty_output><jupyter_text>* 📝 Нам доступны данные пятидесяти ста пользователей, 462 из которых все еще пользуются тарифом:    * 351 – Смарт    * 149 – Ультра* 🆗 Пропусков, которые нужно заполнить, и дубликатов в данных нет;* 📆 Следующие признаки необходимо привести к типу `DateTime`:    * `reg_date` дата подключения тарифа;    * `churn_date` дата прекращения пользования тарифом.  Звонки<jupyter_code>calls.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
RangeIndex: 202607 entries, 0 to 202606
Data columns (total 4 columns):
id           202607 non-null object
call_date    202607 non-null object
duration     202607 non-null float64
user_id      202607 non-null int64
dtypes: float64(1), int64(1), object(2)
memory usage: 6.2+ MB<jupyter_text>⚠️ _«Мегалайн» всегда округляет вверх значения минут. Если пользователь проговорил всего 1 секунду, в тарифе засчитывается целая минута._<jupyter_code>len(calls[calls.duration < 1])
calls.sort_values(['user_id', 'call_date']).head(10)
calls.isna().sum()
calls.duplicated().sum()<jupyter_output><empty_output><jupyter_text>* 📆 `call_date` приведем к `DateTime`;* ❗️ ~20% звонков имеют продолжительность равную меньше 1 минуты. «Мегалайн» всегда округляет вверх значения минут. Выглядит как ошибка в данных. По-хорошему бы сходить к коллегам, которые делали выгрузку и уточнить почему так получилось. Один из вариантов – неправильный метод округления. Сделаем 2 шага для приведения данных в порядок:    1. применим метод `np.ceil` для признака, тем самым получив верхнее значение в минутах;    2. заменим нули на `1` – минимальное значение сессии звонка для оператора.  Сообщения<jupyter_code>messages.info()
messages.head()
messages.isna().sum()
messages.duplicated().sum()<jupyter_output><empty_output><jupyter_text>📆 `message_date` дату сообщения хорошо бы привести к `DateTime`.  Интернет-сессии<jupyter_code>internet.info()
internet.query("user_id == 1001 and session_date == '2018-11-14'")
len(internet.query('mb_used < 1'))
internet.isna().sum()
internet.duplicated().sum()<jupyter_output><empty_output><jupyter_text>* 📝 Объём потраченного за сессию интернет-трафика указан в мегабайтах, в то время, как тарификация в гигабайтах. Таким образом, если пользователь израсходует объем трафика, предоставленный тарифным планом, для анализа нам потребуется привести дополнительный объем трафика к гигабайтам, чтобы рассчитать стоимость;* 📆 `session_date` дату интернет-сессии приведем к `DateTime`.* ❗️ 19 699 сессий имеют объём потраченного за сессию интернет-трафика менее 1 Мб. Как и в случае со звонками, наш оператор округляет вверх значения мегабайтов. Поэтому выполним аналогичные 2 шага для приведения данных в порядок:    1. применим метод `np.ceil` для признака, тем самым получив верхнее значение в минутах;    2. заменим нули на `1` – минимальное значение сессии.  Шаг 2. Подготовим данные  Восстановим пропуски<jupyter_code># Звонки
calls['duration'] = calls['duration'].apply(np.ceil)
nonzero_session = calls['duration'] > 0
calls['duration'].where(nonzero_session, 1, inplace=True)
# Интернет-сессии
internet['mb_used'] = internet['mb_used'].apply(np.ceil)
nonzero_session = internet['mb_used'] > 0
internet['mb_used'].where(nonzero_session, 1, inplace=True)<jupyter_output><empty_output><jupyter_text>Приведем данные к нужным типам<jupyter_code>dtformat = '%Y-%m-%d'

def to_datetime(df, columns):
    for col in columns:
        df[col] = pd.to_datetime(df[col], format=dtformat)

# Даты
to_datetime(users, ['reg_date', 'churn_date'])
to_datetime(calls, ['call_date'])
to_datetime(messages, ['message_date'])
to_datetime(internet, ['session_date'])
# Минуты и мегабайты
calls['duration'] = calls['duration'].astype('int64')
internet['mb_used'] = internet['mb_used'].astype('int64')<jupyter_output><empty_output><jupyter_text>Посчитаем значения, необходимые для анализа каждого пользователя 💡 _Мы рассматриваем выборочные совокупности в разрезе календарных месяцев. В качестве альтернативы могли бы рассмотреть в разрезе расчетного периода оператора._<jupyter_code>costs = pd.DataFrame()<jupyter_output><empty_output><jupyter_text>Количество сделанных звонков и израсходованных минут разговора по месяцам<jupyter_code>calls['month'] = pd.DatetimeIndex(calls['call_date']).month
calls_by_month = calls.pivot_table(
    index=['month', 'user_id'],
    values='duration',
    aggfunc=['count', 'sum']
)
tmp_df = pd.DataFrame(calls_by_month.to_records())
costs['user_id'] = tmp_df['user_id']
costs['month']   = tmp_df['month']
costs['calls']   = tmp_df.iloc[:, 2]
costs['minutes'] = tmp_df.iloc[:, 3]<jupyter_output><empty_output><jupyter_text>Количество отправленных сообщений по месяцам<jupyter_code>messages['month'] = pd.DatetimeIndex(messages['message_date']).month
messages_by_month = messages.pivot_table(
    index=['month', 'user_id'],
    values='id',
    aggfunc='count'
)
tmp_df = pd.DataFrame(messages_by_month.to_records()).rename(columns={'id': 'messages'})
costs = costs.merge(tmp_df, on=['user_id', 'month'], how='outer')<jupyter_output><empty_output><jupyter_text>Объем израсходованного интернет-трафика по месяцам<jupyter_code>internet['month'] = pd.DatetimeIndex(internet['session_date']).month
internet_by_month = internet.pivot_table(
    index=['month', 'user_id'],
    values='mb_used',
    aggfunc='sum'
)
tmp_df = pd.DataFrame(internet_by_month.to_records())
costs = costs.merge(tmp_df, on=['user_id', 'month'], how='outer')
costs.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
Int64Index: 3214 entries, 0 to 3213
Data columns (total 6 columns):
user_id     3214 non-null int64
month       3214 non-null int64
calls       3174 non-null float64
minutes     3174 non-null float64
messages    2717 non-null float64
mb_used     3203 non-null float64
dtypes: float64(4), int64(2)
memory usage: 175.8 KB<jupyter_text>Таким образом, в одном датафрейме мы видим следующую информацию по пользователям в разрезе месяцев:- количество сделанных звонков и израсходованных минут разговора;- количество отправленных сообщений;- объем израсходованного интернет-трафика в мегабайтах. ❗️ После формирования датафрейма по месяцам появились пропуски. Такое ощущение, что некоторые пользователи в некоторые месяцы пользовались не всеми услугами. Например, кто-то только мобильным интернетом. Заполним пропуски нулями и позже посмотрим на распределение.<jupyter_code>na_features = ['calls', 'minutes', 'messages']

for f in na_features:
    # При соединении датафреймов Pandas привел типы к float64 из-за пропусков,
    # поэтому явно приведем их к int64
    costs[f] = costs[f].fillna(0).astype('int64')
costs.info()
costs.head()<jupyter_output><empty_output><jupyter_text>Помесячная выручка с каждого пользователя<jupyter_code># Для удобства добавим в датафрейм информацию о пользователях и тарифах
costs = costs.merge(users, on='user_id', how='left').merge(tariffs, on='tariff', how='left')
# Перерасход минут, сообщений и трафика посчитаем как разность между месячным значением и включенным в тариф
costs['minutes_over']  = costs['minutes'] - costs['minutes_included']
costs['messages_over'] = costs['messages'] - costs['messages_included']
costs['mb_used_over']  = costs['mb_used'] - costs['mb_per_month_included']
costs.head()<jupyter_output><empty_output><jupyter_text>⚠️ Положительные значения указывают на перерасход. Отрицательные на остаток в рамках тарифа.<jupyter_code>def calc_monthly_revenue(row):
    """
    Вычтем бесплатный лимит из суммарного количества звонков, сообщений и интернет-трафика,
    остаток умножим на значение из тарифного плана.
    """
    
    minutes_price = 0
    messages_price = 0
    mb_used_price = 0
    
    # стоимость дополнительных минут
    if row['minutes_over'] > 0:
        minutes_price = row['minutes_over'] * row['rub_per_minute']

    # стоимость дополнительных сообщений
    if row['messages_over'] > 0:
        messages_price = row['messages_over'] * row['rub_per_message']

    # стоимость дополнительного трафика
    if row['mb_used_over'] > 0:
        mb_used_price = (row['mb_used_over'] / 1024) * row['rub_per_gb']
    
    return minutes_price + messages_price + mb_used_price
costs['rub_monthly_fee_over'] = costs.apply(calc_monthly_revenue, axis=1)
costs['rub_monthly_fee_total'] = costs['rub_monthly_fee'] + costs['rub_monthly_fee_over']
# Датафрейм только с нужными нами данными
costs = pd.DataFrame(
    costs[['month', 'tariff', 'user_id', 'city',
           'calls', 'minutes', 'messages', 'mb_used',
           'minutes_over', 'messages_over', 'mb_used_over',
           'rub_monthly_fee_over', 'rub_monthly_fee_total']]
)
costs.head()
smart_costs = costs.query("tariff == 'smart'")
len(smart_costs.groupby('user_id'))
ultra_costs = costs.query("tariff == 'ultra'")
len(ultra_costs.groupby('user_id'))<jupyter_output><empty_output><jupyter_text>📝 В нашей результирующей таблицы оказались записи по 350 пользователям тарифа Смарт и 148 тарифа Ультра. Информации о расходах 2х пользователей не нашлось.  Шаг 3. Анализируем данныеОпишем поведение клиентов оператора, исходя из выборки.<jupyter_code>values = ['minutes', 'messages', 'mb_used', 'rub_monthly_fee_over', 'rub_monthly_fee_total']
# Сделаем сводную таблицу по тарифам в разрезе месяцев
tariff_monthly_costs = costs.pivot_table(
    index=['tariff', 'month'],
    values=values
)
smart_monthly_costs = tariff_monthly_costs.loc['smart']
smart_monthly_costs
ultra_monthly_costs = tariff_monthly_costs.loc['ultra']
ultra_monthly_costs<jupyter_output><empty_output><jupyter_text>Минуты разговора, количество сообщений и объём интернет-трафика в среднем необходимые пользователям каждого тарифа в месяц<jupyter_code>tariff_usage = tariff_monthly_costs.pivot_table(
    index='tariff',
    values=values
)
tariff_usage<jupyter_output><empty_output><jupyter_text>💡 Наблюдения* Пользователи тарифа Ультра более активны, но пользователи тарифа Смарт тратят в среднем в 6 раз больше на дополнительные пакеты услуг. Оператор может использовать это для маркетинговой компании.* Оператору больше денег приносит тариф Ультра, даже с учетом того, что в среднем пользователи не выходят за границы тарифных лимитов, а пользователи тарифа Смарт не тратят столько денег, чтобы перегнать стоимость тарифа Ультра. Таким образом, для пользователя выгоднее тариф Смарт.  Москва и другие регионы<jupyter_code>moscow = costs['city'] == 'Москва'
costs['city'].where(moscow, 'Другой регион', inplace=True)

city_monthly_costs = costs.pivot_table(
    index=['city', 'month'],
    values=values
)
city_usage = city_monthly_costs.pivot_table(
    index='city',
    values=values
)
city_usage<jupyter_output><empty_output><jupyter_text>💡  НаблюденияПо средним значениям выручки пользователей Москвы и регионов мы видим, что разница не велика. Пользователи примерно отправляют одинаковое количество смс и звонят примерно одинаковое количество минут. В Москве чуть больше потребляют трафика – ожидаемо с учетом покрытия и скорости. Насколько получившиеся значения случайны мы узнаем при проверке гипотезы.  Дисперсия и стандартное отклонениеНормальное распредление определяют среднее и дисперсия. Они позволяют понять насколько шумные сейчас данные.  Минуты разговора<jupyter_code>smart_costs['minutes'].describe()
ultra_costs['minutes'].describe()
plt.hist(smart_costs['minutes'], bins=50, label='smart')
plt.hist(ultra_costs['minutes'], bins=50, label='ultra', alpha=0.7)
plt.legend(loc='upper right')
plt.show()
smart_minutes_var = np.var(smart_costs['minutes'], ddof=1)
ultra_minutes_var = np.var(ultra_costs['minutes'], ddof=1)
smart_minutes_var
ultra_minutes_var<jupyter_output><empty_output><jupyter_text>Результаты:| Метрика                   | Смарт    | Ультра     ||:-------------------------|----------|-----------|| Стандартное отклонение (мин) | 194.87    | 325.73    || Дисперсия                 | 37,974.77 | 106,105.72 |  Количество сообщений<jupyter_code>smart_costs['messages'].describe()
ultra_costs['messages'].describe()
plt.hist(smart_costs['messages'], bins=50, label='smart')
plt.hist(ultra_costs['messages'], bins=50, label='ultra', alpha=0.7)
plt.legend(loc='upper right')
plt.show()
smart_messages_var = np.var(smart_costs['messages'], ddof=1)
ultra_messages_var = np.var(ultra_costs['messages'], ddof=1)
smart_messages_var
ultra_messages_var<jupyter_output><empty_output><jupyter_text>Результаты:| Метрика                        | Смарт    | Ультра    ||:------------------------------|---------|-----------|| Стандартное отклонение (сообщения) | 28.22    | 47.80    || Дисперсия                      | 796.81   | 2,285.27 |  Интернет-трафик<jupyter_code>smart_costs['mb_used'].describe()
ultra_costs['mb_used'].describe()
plt.hist(smart_costs['mb_used'], bins=50, label='smart')
plt.hist(ultra_costs['mb_used'], bins=50, label='ultra', alpha=0.7)
plt.legend(loc='upper right')
plt.show()
smart_mb_used_var = np.var(smart_costs['mb_used'], ddof=1)
ultra_mb_used_var = np.var(ultra_costs['mb_used'], ddof=1)
smart_mb_used_var
ultra_mb_used_var<jupyter_output><empty_output><jupyter_text>Результаты:| Метрика                         | Смарт         | Ультра         ||:-------------------------------|---------------|---------------|| Стандартное отклонение (Мб)        | 5,871.04      |  9,952.83     || Дисперсия                        | 34,469,075.73 | 99,058,834.60 | 🙇🏼‍♂️ _Перед тем, как делать расчеты и строить сводные таблицы, мы могли бы избавиться от выбросов в данных, но тогда был бы риск потерять часть датасета за какой-то временной промежуток из наших выборочных совокупностей._  Шаг 4. Проверим гипотезыМы рассматриваем две генеральные сопокупности – пользователей разных тарифов мобильного оператора. Проверим две (двусторонние) гипотезы о равенстве среднего двух генеральных совокупностей по взятым из них выборкам. Чтобы понять, различается ли средняя выручка пользователей разных тарифов и населенных пунктов, значима ли разница между средними значениями, проведем статистические тесты.  Cредняя выручка пользователей тарифов «Ультра» и «Смарт» различается* Средняя выручка пользователя тарифа "Смарт":  1131.48 руб* Средняя выручка пользователя тарифа "Ультра": 2039.91 рубНулевая гипотеза H₀:> Средняя выручка пользователей тарифов "Ультра" и "Смарт" **не различается**Исходя из H₀ формулируется альтернативная гипотеза H₁:> Средняя выручка пользователей тарифов "Ультра" и "Смарт" **различается**Пороговое значение `alpha` (критический уровень статистической значимости) зададим равным `1%`.<jupyter_code>alpha = .01

results = st.ttest_ind(
    smart_costs['rub_monthly_fee_total'], 
    ultra_costs['rub_monthly_fee_total'])

print('p-значение:', results.pvalue)

if (results.pvalue < alpha):
    print("Отвергаем нулевую гипотезу")
else:
    print("Не получилось отвергнуть нулевую гипотезу")<jupyter_output>p-значение: 8.009674149482176e-180
Отвергаем нулевую гипотезу<jupyter_text>Средняя выручка по тарифам не одинакова, и практически нулевая вероятность говорит о том, что случайно получить такое отличие в значениях практически не получится. Следовательно, средняя выручка пользователей тарифов "Ультра" и "Смарт" действительно различается и наша гипотеза подтвердилась.  Cредняя выручка пользователей из Москвы отличается от выручки пользователей из других регионов* Средняя выручка пользователей из Москвы:          1464.53 руб* Средняя выручка пользователей из других регионов: 1387.79 рубНулевая гипотеза H₀:> Cредняя выручка пользователей из Москвы **не отличается** от выручки пользователей из других регионовИсходя из H₀ формулируется альтернативная гипотеза H₁:> Cредняя выручка пользователей из Москвы **отличается** от выручки пользователей из других регионовПороговое значение `alpha` (критический уровень статистической значимости) зададим равным `5%`.<jupyter_code>alpha = .05

moscow = costs.query("city == 'Москва'")['rub_monthly_fee_total']
other_cities = costs.query("city == 'Другой регион'")['rub_monthly_fee_total']

results = st.ttest_ind(moscow, other_cities)

print('p-значение:', results.pvalue)

if (results.pvalue < alpha):
    print("Отвергаем нулевую гипотезу")
else:
    print("Не получилось отвергнуть нулевую гипотезу")<jupyter_output>p-значение: 0.5220687323801865
Не получилось отвергнуть нулевую гипотезу
{'fraction_non_alphanumeric': 0.11101044553400127, 'fraction_numerical': 0.021743764655723728, 'mean_word_length': 5.67556029882604, 'pattern_counts': {'":': 2, '<': 99, '<?xml version=': 0, '>': 104, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '24', 'dir': 'jupyter-structured-clean-dedup', 'id': '168789', 'license': 'MIT', 'n_tokens_mistral': 7622, 'n_tokens_neox': 8382, 'n_words': 1656, 'path': 'sprint4/notebook.ipynb', 'repo_name': 'akrisanov/yandex-praktikum-data-science', 'size': '18764'}