# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# **Поля**
# 1. school — аббревиатура школы, в которой учится ученик
# 2. sex — пол ученика ('F' - женский, 'M' - мужской)
# 3. age — возраст ученика (от 15 до 22)
# 4. address — тип адреса ученика ('U' - городской, 'R' - за городом)
# 5. famsize — размер семьи('LE3' <= 3, 'GT3' >3)
# 6. Pstatus — статус совместного жилья родителей ('T' - живут вместе 'A' - раздельно)
# 7. Medu — образование матери (0 - нет, 1 - 4 класса, 2 - 5-9 классы, 3 - среднее специальное или 11 классов, 4 - высшее)
# 8. Fedu — образование отца (0 - нет, 1 - 4 класса, 2 - 5-9 классы, 3 - среднее специальное или 11 классов, 4 - высшее)
# 9. Mjob — работа матери ('teacher' - учитель, 'health' - сфера здравоохранения, 'services' - гос служба, 'at_home' - не работает, 'other' - другое)
# 10. Fjob — работа отца ('teacher' - учитель, 'health' - сфера здравоохранения, 'services' - гос служба, 'at_home' - не работает, 'other' - другое)
# 11. reason — причина выбора школы ('home' - близость к дому, 'reputation' - репутация школы, 'course' - образовательная программа, 'other' - другое)
# 12. guardian — опекун ('mother' - мать, 'father' - отец, 'other' - другое)
# 13. traveltime — время в пути до школы (1 - <15 мин., 2 - 15-30 мин., 3 - 30-60 мин., 4 - >60 мин.)
# 14. studytime — время на учёбу помимо школы в неделю (1 - <2 часов, 2 - 2-5 часов, 3 - 5-10 часов, 4 - >10 часов)
# 15. failures — количество внеучебных неудач (n, если 1<=n<=3, иначе 0)
# 16. schoolsup — дополнительная образовательная поддержка (yes или no)
# 17. famsup — семейная образовательная поддержка (yes или no)
# 18. paid — дополнительные платные занятия по математике (yes или no)
# 19. activities — дополнительные внеучебные занятия (yes или no)
# 20. nursery — посещал детский сад (yes или no)
# 21. higher — хочет получить высшее образование (yes или no)
# 22. internet — наличие интернета дома (yes или no)
# 23. romantic — в романтических отношениях (yes или no)
# 24. famrel — семейные отношения (от 1 - очень плохо до 5 - очень хорошо)
# 25. freetime — свободное время после школы (от 1 - очень мало до 5 - очень мого)
# 26. goout — проведение времени с друзьями (от 1 - очень мало до 5 - очень много)
# 27. health — текущее состояние здоровья (от 1 - очень плохо до 5 - очень хорошо)
# 28. absences — количество пропущенных занятий
# 29. score — баллы по госэкзамену по математике
#
# **Задача**
#  * Проведите первичную обработку данных. Так как данных много, стоит написать функции, которые можно применять к столбцам определённого типа.
#  * Посмотрите на распределение признака для числовых переменных, устраните выбросы.
#  * Оцените количество уникальных значений для номинативных переменных.
#  * По необходимости преобразуйте данные
#  * Проведите корреляционный анализ количественных переменных
#  * Отберите не коррелирующие переменные.
#  * Проанализируйте номинативные переменные и устраните те, которые не влияют на предсказываемую величину (в нашем случае — на переменную score).
#  * Не забудьте сформулировать выводы относительно качества данных и тех переменных, которые вы будете использовать в дальнейшем построении модели.
#

# +
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from itertools import combinations
from scipy.stats import ttest_ind
import math as math

pd.set_option('display.max_rows', 50)  # показывать больше строк
pd.set_option('display.max_columns', 50)  # показывать больше колонок

df = pd.read_csv('stud_math.csv')

# -

# Так, теперь посмотрим что у нас в данных с смысле сигнал/шум

# +
# and check some numbers
def is_numeric_series(s):
    return pd.api.types.is_integer_dtype(series) or\
    pd.api.types.is_float_dtype(series)

numeric_fields = []
string_fields = []

for (columnName, columnData) in df.iteritems():
    series = pd.Series(columnData)
    if(is_numeric_series(series)):
        numeric_fields.append(columnName)
        sns.boxplot(series)
        print(series.describe())
        plt.show()
    else:
        string_fields.append(columnName)

# ok, what do we have here?
df[string_fields].apply(lambda s: print(s.value_counts(dropna=False), '[',
                        s.isna().sum(), '/',
                        s.value_counts(dropna=False).sum(),
                        '] NaN'))

# -

# Самый пустой столбец - _PStatus_, но в принципе данные не выглядят слишком побитыми  
#
# *Философские вопросы/соображения:*  
#  1. можно ли `NaN` объединить с other там, где есть этот вариант? Звучит логично по крайней мере для _Mjob/Fjob/reason/guardian_, так как в обоих случаях данных нет. Хотя, возможно, логичней было бы заменить other на `NaN`, чтобы иллюзий не питать?)  
#  2. _Fedu_ `40` — единственный адский выброс — я бы (в нарушение канона?) счел пропущенной точкой в `4.0`, но все же буду чистить, не сделает он погоды, почищу как не входящий в заявленную размерность.  
#  3. _absences_ — жуткие выбросы, но количество пропусков по идее сильно влияет на успеваемость, может, попробовать сделать еще столбец про них, с размерностью типа `1..4` (нет, не много, многовато, огого сколько), должна быть сильная корреляция. Интересно, это в часах? С первого класса?  
#  4. _age_, _Medu_, _traveltime_, _studytime_, _studytime, granular_, _failures_, _freetime_, _goout_ — не надо чистить выбросы, даже если они формально есть: данные попадают в размерность, все логично  
#  5. _famrel_ убрать `-1`  
#  6. В строковых данных синонимизировать ничего не надо, только с `NaN` разобраться (кроме проверки идеи из п 1).  
#  7. В _score_ — интересно, что значит `0`? Не пришел? Интересно, что это в принципе значит.
#
# По-хорошему, конечно, надо _absences_ почистить и сравнить с предположением. Хотя `400` — за гранью, конечно, значение. 
#
# Наверное, стоит проанализировать выкинув по-честному все что не подходит, второй анализ провести.

# +
def break_into_quantiles_and_outliers_bins(frame, col_name, suffix):
    q1_temp = frame[col_name].quantile(0.25, interpolation='midpoint')
    q2_temp = frame[col_name].quantile(0.5, interpolation='midpoint')
    q3_temp = frame[col_name].quantile(0.75, interpolation='midpoint')
    iqr = q3_temp - q1_temp
    frame[col_name + suffix] = frame[col_name].apply(lambda x: 0 if (q1_temp - 1.5 * iqr > x) else (
        1 if x >= q1_temp - 1.5 * iqr and x < q1_temp else (
        2 if x >= q1_temp and x < q2_temp else (
        3 if x >= q2_temp and x < q3_temp else (
        4 if x >= q3_temp and q3_temp + 1.5 * iqr >= x else 5)))))


def clean_outliers(frame, col_name, suffix):
    q1_temp = frame[col_name].quantile(0.25, interpolation='midpoint')
    q3_temp = frame[col_name].quantile(0.75, interpolation='midpoint')
    iqr = q3_temp - q1_temp
    has_outliers = q3_temp + 1.5 * \
        iqr < frame[col_name].max() or q1_temp - 1.5 * \
        iqr > frame[col_name].min()
    frame[col_name + suffix] = frame[col_name].apply(lambda x: None if (
        q3_temp + 1.5 * iqr < x or q1_temp - 1.5 * iqr > x) else x)


def clean_out_of_range(frame, col_name, suffix, min_val, max_val):
    frame[col_name + suffix] = frame[col_name].apply(
        lambda x: None if (x < min_val or x > max_val) else x)


# clean up to fit in range
clean_out_of_range(df, 'Medu', '', 1, 4)
clean_out_of_range(df, 'Fedu', '', 1, 4)
clean_out_of_range(df, 'famrel', '', 1, 4)

# first pass on absences
clean_outliers(df, 'absences', '_flat_cleaned')
# print(df.absences_flat_cleaned.value_counts(dropna = False))

# and a test of data mapping
break_into_quantiles_and_outliers_bins(
    df, 'absences', '_experimentally_mapped')
# print(df.absences_experimentally_mapped.value_counts(dropna = False))

# try to join NaN with 'other' to compare
for item in ['Mjob', 'Fjob', 'reason', 'guardian']:
    df[item + '_other_joined'] = df[item].apply(
        lambda x: 'other' if pd.isnull(x) else x)

# cleanup NaNs from all string columns - just to be sure
for item in string_fields:
    df[item] = df[item].apply(lambda x: None if pd.isnull(x) else x)

# -

# Итак, вроде все удалось почистить, + нагенерил еще несколько дополнительных полей, дальнейший план:  
# 1. Посмотреть `corr()`, чтобы оценить как поведение, описать выводы, отдельно посмотреть на соответствие _absences_flat_cleaned_ и _absences_experimentally_mapped_
# 2. Потом прогнать `boxplot()` по всем строковым полям, отдельно посмотреть на отношение `_other_joined` вариантов к обычным почищенным
#     
#     

df.corr()


# Итак, первичные количественные выводы:  
#  1. Основные положительные факторы - образование родителей, причем *образование матери сильнее всего* (проверить с _guardian_), время учебы (кто бы мог подумать)  
#  2. Второстепенные положительные факторы (по убыванию) - пропуски (не могу понять как так получилось, надо копать глубже), свободное время, отношения в семье влияют совсем слабо  
#  3. Отрицательные факторы (по убыванию) - прочие неудачи, возраст (проверить с _romantic_), время с друзьями, время до школы, здоровье (странно)
#

# df[(df.guardian == 'father')].corr()
df[(df.guardian == 'mother')].corr()


df[(df.Pstatus == 'A') & (df.guardian == 'father')].corr()


df[(df.romantic == 'yes')].corr()


# Чуть вглубь:
#  1. Обратная корреляция с возрастом перестает играть роль, если выбрать только тех, кто в романтических отношениях, хотя вполне возможно, дело в возрасте в первую очередь  
#  2. Образование матери все равно влияет сильнее, чем образование отца, причем если опекун - отец, разница в ~10 раз, а если опекун - мать, то меньше, чем вдвое. Даже те, у кого родители живут отдельно и опекун - отец, все равно сильнее подвержены влиянию образования матери.
#  

# +
# convert everything to number
bool_fields = ['internet', 'romantic', 'higher',
               'nursery', 'activities', 'paid', 'famsup', 'schoolsup']

for item in bool_fields:
    df[item + '_to_num'] = df[item].apply(
        lambda x: None if pd.isnull(x) else (1 if x == 'yes' else 0))

df.corr()


# +
def show_boxplot(frame, column):
    fig, ax = plt.subplots(figsize=(5, 4))
    sns.boxplot(x=column, y='score', data=frame, ax=ax)
    plt.xticks(rotation=45)
    ax.set_title('Boxplot for ' + column)
    plt.show()

# take a look at remaining strings
for item in string_fields:
    if(item not in bool_fields):
        show_boxplot(df, item)
        if item + '_other_joined' in df:
            show_boxplot(df, item + '_other_joined')

# -

# Гипотеза о том, что можно получить что-то более осмысленное, объединив `NaN` и `other` не подтвердилась.

# everything else with two values to number
df['school_to_num'] = df['school'].apply(
    lambda x: None if pd.isnull(x) else (1 if x == 'MS' else 0))
df['sex_to_num'] = df['sex'].apply(
    lambda x: None if pd.isnull(x) else (1 if x == 'M' else 0))
df['address_to_num'] = df['address'].apply(
    lambda x: None if pd.isnull(x) else (1 if x == 'R' else 0))
df['famsize_to_num'] = df['famsize'].apply(
    lambda x: None if pd.isnull(x) else (1 if x == 'LE3' else 0))
df['Pstatus_to_num'] = df['Pstatus'].apply(
    lambda x: None if pd.isnull(x) else (1 if x == 'T' else 0))
df['guardian_to_num'] = df['guardian'].apply(lambda x: None if (
    pd.isnull(x) or x == 'other') else (1 if x == 'father' else 0))
df.corr()


# +
# Take every column with correlation > 0.1
# add Mjob and Fjob because their medions are different as can be seen on boxplots
# add paid, shoolsup, sex which have 0.9999..0.88 correlation to score, which seems not enough, however somehow close, which might be useful for some extended selections
# absences not included because 0.9 correlation was only found in a field which aggregated all extreme and unrealistic outliers, so it doesn't seem to be suitable

df_for_model = df.loc[:, ['score', 'age', 'Medu',
                          'Fedu', 'studytime', 'failures', 'goout']]
df_for_model['romantic'] = df['romantic_to_num']
df_for_model['address'] = df['address_to_num']
df_for_model['Mjob'] = df['Mjob']
df_for_model['Fjob'] = df['Fjob']
df_for_model['paid'] = df['paid_to_num']
df_for_model['schoolsup'] = df['schoolsup_to_num']
df_for_model['sex'] = df['sex_to_num']

display(df_for_model.corr())
show_boxplot(df_for_model, 'Mjob')
show_boxplot(df_for_model, 'Fjob')

# -

# **Итого, основные корреляции:**
# 1. Положительно - образование родителей, особенно матери, и время учебы.
# 2. Отрицательно - жизненные неудачи, романтические отношения, возраст, время на улице, жизнь в сельской местности
# 3. Слабо коррелируют, но, возможно, пригодятся для дополнительной фильтрации - платные курсы, образовательная поддержка в школе, пол, профессия родителей
#
# Между сильной и слабой корреляцией я проводил границу по значению `0.1`, то есть, строго говоря, это довольно условно.

# *К сожалению, был огромный завал последний месяц, поэтому кучу всего не успел, да и на модуль не было времени сколько хотел*



StarcoderdataJupyterScriptsDedupFiltered