Данные для этого EDA были получены по этой ссылке. Зависимости для этого EDA — это pandas, numpy и sklearn. Код будет предоставлен под каждым соответствующим разделом.

import pandas as pd
import numpy as np

from sklearn.metrics import mean_squared_error
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsRegressor, NearestNeighbors
from sklearn.model_selection import train_test_split, GridSearchCV,\
cross_val_score
from sklearn.tree import DecisionTreeRegressor
from sklearn.linear_model import LinearRegression, Ridge
from sklearn.metrics import mean_squared_error

#all relevant imports used for this EDA

df = pd.read_csv('data/climber_df.csv')

#reading in the data

Набор данных, используемый для этого анализа, представляет собой набор данных альпиниста, взятый с популярного альпинистского сайта 8a.nu. Набор данных содержит более 10 000 записей с подробной информацией о альпинистах, такой как вес, рост и годы альпинизма, а также информацию о восхождениях, которые они зарегистрировали на сайте. Я буду изучать некоторые методы машинного обучения, чтобы найти лучший предсказатель средней оценки скалолазов. Для этого анализа моей метрикой для определения эффективности модели будет среднеквадратическая ошибка.

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

df_clean = df[[
'sex',
'height',
'weight',
'age',
'years_cl',
'grades_first',
'grades_mean'
]]

#creating a new dataframe based on relevant features

При изучении удаления выбросов выбросы в этом наборе данных можно отнести к естественной дисперсии данных; поэтому их не следует удалять. Удаление выбросов приведет к удалению всех женщин-альпинисток из набора данных, поскольку они представляют только примерно 12,5% данных. Данные были сохранены относительно неизменными, поскольку набор данных уже был чистым.

def remove_outliers(data, columns):
    for x in columns:
        q1 = data.quantile(0.25)
        q3 = data.quantile(0.75)
        data = data[(data[x] >= q1[x]) & (data[x] <= q3[x])] 
        return data

#function removing values below the 25th percentile and
#above the 75th percentile

df_clean = remove_outliers(df_clean,list(df_clean.columns))

#calling the function on the DataFrame

Теперь, когда данные чистые, X и y были определены, и было проведено разделение тестов поезда.

X = df_clean.drop('grades_mean', axis=1)
y = df_clean['grades_mean']
X_train, X_test, y_train, y_test = train_test_split(X, y)

#defining our features, X, and target, y and conducting
#a train test split

В качестве базовой модели была выбрана модель линейной регрессии. Модель работала довольно хорошо со среднеквадратичной ошибкой (RMSE) примерно 4,040143. Затем тестовые данные масштабировались; однако это не повлияло на RMSE. В целом, это надежный базовый уровень, учитывая распределение данных.

baseline = LinearRegression().fit(X_train, y_train)

#instantiating the linear regression model and fitting
#it to X_train and y_train

baseline_score = cross_val_score(baseline,
                                X_train,
                                y_train,
                                scoring='neg_root_mean_squared_error')

#cross validation

baseline_rmse = abs(baseline_score.mean())
baseline_rmse

#finding the root mean squared error
#the output of the above code is 4.040143002484213

ss = StandardScaler()

X_train_scaled = ss.fit_transform(X_train)
X_test_scaled = ss.transform(X_test)

#scaling the data

Следующей моделью для тестирования была регрессия k-ближайших соседей (KNN). Эта модель немного сложнее, чем модель линейной регрессии; поэтому я использовал GridSearchCV, чтобы найти оптимальные параметры для минимизации RMSE. Первый поиск по сетке дал среднеквадратичное отклонение примерно 4,209931, что хуже, чем у базовой модели. Однако лучшим параметром для n_neighbors было 15, что является максимальным значением, которое я указал при поиске по сетке. Поскольку это максимальное значение, нам нужно будет провести еще один поиск по сетке с большими значениями для n_neighbors, чтобы увидеть, можно ли еще больше минимизировать RMSE.

knn = KNeighborsRegressor()

#instantiating the k-nearest neighbors regressor

grid = {
    'n_neighbors': [1,3,5,7,9,11,13,15],
    'metric': ['minkowski','manhattan'],
    'weights': ['uniform','distance'],
    'algorithm': ['ball_tree', 'kd_tree', 'brute']
}

#defining a grid with parameters for the grid search

gs1 = GridSearchCV(estimator=knn,
                   param_grid=grid,
                   scoring='neg_root_mean_squared_error')

#conducting the grid search with the above grid

gs1.fit(X_train_scaled, y_train)

#fitting the grid to the scaled data

abs(gs1.best_score_)

#output: 4.2099312358726

gs1.best_params_

#output:
#{'algorithm': 'kd_tree',
#'metric': 'minkowski',
#'n_neighbors': 15,
#'weights': 'uniform'}

Вторая сетка задавала значения n_neighbors от 15 до 50 с шагом 5. Эта модель дала немного лучший результат со среднеквадратичным отклонением примерно 4,154233. Новое лучшее значение для n_neighbors равно 35. Поскольку это не минимум и не максимум, мы можем остановить поиск сетки здесь.

grid2 = {
    'n_neighbors': [15,20,25,30,35,40,45,50],
    'metric': ['minkowski','manhattan'],
    'weights': ['uniform','distance'],
    'algorithm': ['ball_tree', 'kd_tree', 'brute']
}

#defining a new grid with updated values for n_neighbors

gs2 = GridSearchCV(estimator=knn,
                   param_grid=grid2,
                   scoring='neg_root_mean_squared_error').fit(X_train_scaled, y_train)

#running and fitting the grid search

abs(gs2.best_score_)

#output: 4.154233260777203

gs2.best_params_

#output:
#{'algorithm': 'kd_tree',
# 'metric': 'minkowski',
# 'n_neighbors': 35,
# 'weights': 'uniform'}

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

Еще одна модель, которую следует рассмотреть, — это регрессионная модель дерева решений. Опять же, мы будем использовать GridSearchCV, чтобы найти оптимальные параметры для минимизации RMSE. Потребовалась только одна итерация сетки, поскольку идеальные параметры не были минимальными или максимальными из предоставленных параметров. Лучшая модель дерева решений имела RMSE примерно 4,177210, что немного хуже, чем у нашей лучшей модели KNN, и немного хуже, чем у нашего базового уровня. Регрессор дерева решений также не является хорошим предсказателем для этих данных.

dt = DecisionTreeRegressor(random_state=42)

grid3 = {
    'splitter': ['best', 'random'],
    'max_depth': [None ,5, 10, 15, 20, 25, 30, 35, 40, 45, 50],
    'min_samples_split': [2, 4, 6, 8, 10, 12, 14, 16, 18, 20],
    'min_samples_leaf': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
}

gs3 = GridSearchCV(estimator=dt,
                   param_grid=grid3,
                   scoring='neg_root_mean_squared_error').fit(X_train, y_train)
}

#the same process described above, except with decision
#trees instead of KNN

abs(gs3.best_score_)

#output: 4.040142826542541

gs3.best_params_

#output:
#{'max_depth': 5,
# 'min_samples_leaf': 7,
# 'min_samples_split': 2,
# 'splitter': 'best'}

Поскольку кажется, что наша модель линейной регрессии работает лучше для этих данных, мы можем протестировать другую линейную модель. Последняя модель, которую мы проанализируем, — это модель гребневой регрессии. Для различных значений альфа-канала была определена сетка, и для поиска наилучшего значения альфа-канала использовался GridSearchCV. Лучшая модель гребневой регрессии имела RMSE 4,041428, что немного лучше, чем у нашей модели линейной регрессии. Модель гребневой регрессии представляется лучшей базовой моделью для этого набора данных.

ridge_model = Ridge(random_state=42)

grid4 = {'alpha':[1,10,25,50,75,100,125,250,500,750,1000,2500]}

gs4 = GridSearchCV(estimator=ridge_model,
                   param_grid=grid4,
                   scoring='neg_root_mean_squared_error').fit(X_train_scaled, y_train)

#the same process described above, except with ridge regression

abs(gs4.best_score_)]

#output: 4.040142826542541

gs4.best_params_

#{'alpha': 1}

Теперь мы определяем лучшую модель гребневой регрессии в качестве нашей окончательной модели и анализируем тестовую среднеквадратичную ошибку. В результате было получено значение 4,103683, что указывает на то, что модель работает хорошо.

final_model = Ridge(random_state=42).fit(X_train_scaled, y_train)
y_pred_test = final_model.predict(X_test_scaled)
test_rmse = mean_squared_error(y_pred_test, y_test, squared=False)
test_rmse

#output: 4.103683007366169

Наконец, мы можем преобразовать истинные и предсказанные значения в соответствующие им оценки, используя таблицу преобразования оценок. Этот процесс требует округления истинных и предсказанных значений, а затем присвоения им соответствующей оценки по французскому языку. Учитывая, что предоставленная система оценок включает / оценки (например, 6a/+ означает, что оценка либо 6a, либо 6a+), RMSE приблизительно 4,04 обеспечивает довольно точное предположение относительно средней оценки альпинистов.

y_pred = final_model.predict(X_train_scaled)

#defining the predictions with our final model

convert = pd.read_csv('data/grades_conversion_table.csv').drop('Unnamed: 0', axis=1)

#reading in the coversion DataFrame

adjusted_pred = []
for x in y_pred:
    if x - math.floor(x) < 0.5:
        adjusted_pred.append(math.floor(x))
    else:
        adjusted_pred.append(math.ceil(x))
        
adjusted_y = []
for x in y_train:
    if x - math.floor(x) < 0.5:
        adjusted_y.append(math.floor(x))
    else:
        adjusted_y.append(math.ceil(x))

#this for loop takes each true/predicted value and
#returns the ceiling or floor based on whether the
#decimal of the number is < 0.5

np.array(adjusted_pred)
np.array(adjusted_y)

pred_grade = []
for z in adjusted_pred:
    for x, y in convert.iterrows():
        if z == x:
            pred_grade.append(y[1])
            
y_grade = []
for z in adjusted_y:
    for x, y in convert.iterrows():
        if z == x:
            y_grade.append(y[1])

#this loop takes the rounded true/prediction values
#and assigns them to the corresponding grade from
#the grade conversion DataFrame

np.array(pred_grade)
np.array(y_grade)

pd.DataFrame({
    'True Value':adjusted_pred,
    'Predition':adjusted_y,
    'True Grade':y_grade,
    'Predicted Grade':pred_grade
})

#final table showing the true vs. predictions

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

Это базовое применение алгоритмов машинного обучения позволило найти достойный предиктор средней оценки альпинистов на основе таких важных характеристик, как пол, возраст и рост. В этом процессе использовалось очень простое моделирование и GridSearchCV, чтобы найти оптимальные параметры. Следующие шаги для этого EDA будут включать более сложные методы машинного обучения для поиска лучшей модели. Преобразование оценок в другую систему оценок может быть интересным, так как системы оценок скалолазания, как правило, не совпадают.