В этом проекте использовался итеративный подход к построению модели множественной линейной регрессии с помощью python, scikit-learn и statsmodels для прогнозирования продажных цен на дома в округе Кинг, штат Вашингтон, с использованием данных о домах, проданных в 2014 и 2015 годах (данные предоставлены Flatiron School и аналогичный набор данных доступен на Kaggle). Первая часть этого проекта тратится на очистку данных, разработку нескольких новых функций, а затем на построение моделей, настройку каждой модели до тех пор, пока не будет построена окончательная модель. Вместо того, чтобы использовать преобразования журнала для нормализации данных, я решил сохранить модель как можно более интерпретируемой, используя альтернативные методы для нормализации остатков. В результате я сузил данные, чтобы спрогнозировать дома менее 900 тыс., Отдав предпочтение прогнозам для семей среднего класса. Полный блокнот доступен здесь: https://github.com/dbarth411/dsc-mod-2-project-v2-1-online-ds-sp-000.

Вот список описаний столбцов, так как некоторые имена столбцов неоднозначны:

Вот информационный снимок данных:

#Import and preview the data
df = pd.read_csv('kc_house_data.csv')
display(df.info())
df.head()

Имеется 20 столбцов и более 21 тыс. Строк данных. В нескольких столбцах были пропущенные значения и определенные значения-заполнители, которые необходимо было соответствующим образом заменить. Заполнить отсутствующие значения для этого набора данных было довольно просто, поэтому я не буду анализировать то, что сделал в этой публикации. Вместо этого я сразу перейду к инженерным функциям.

Разработка функций

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

#Convert date column to 2 separate columns for month and year
date = df['date'].str.split('/', expand=True)
df['month_sold'] = date[0].astype('float64')
df['year_sold'] = date[2].astype('float64')
#Drop original date column
df.drop(columns=['date'], axis=1, inplace=True)

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

df['age'] = 2015 - df.yr_built
df = df.drop(columns=['yr_built'], axis=1)

В-третьих, я изменил столбец года обновления на двоичный столбец. 1 для домов, отремонтированных за последние 10 лет или построенных за последние 5 (скорее всего, не нуждающихся в ремонте), и 0 для домов, которые не ремонтировались в течение 10 лет.

#Fill missing values
df.yr_renovated.fillna(0.0, inplace=True)
#Create renovated column
df['renovated'] = df.year_sold - df.yr_renovated
#Replace any values less than 10 with 1, and any values over 10 with 0renovated = df.renovated.values
age = df.age.values
values = np.where(renovated <= 10, 1, 0)
df['renovated'] = np.where(age <= 5, 1, values)
#Drop yr_renovated column
df.drop(columns=['yr_renovated'], axis=1, inplace=True)

Наконец, простая диаграмма рассеяния Seaborn показывает дома по их широте и долготе с ценой, установленной в качестве оттенка.

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

from geopy import distance
lat_long = df['lat'].astype(str) + ',' + df['long'].astype(str)
lat_long = list(map(eval, lat_long))
Seattle = (47.6062, -122.3321)
miles = []
for i in lat_long:
    miles.append(round(distance.distance(i, Seattle).miles, 1))
df['distance'] = miles

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

#Function for linear regression
def linear_model(dataframe):
    '''Build linear regression model, return model and print model summary from statsmodels.'''
    #Create feature and target columns
    X = dataframe.drop(columns=['price'], axis=1)
    y = dataframe.price
    
    #Split data into training and test sets
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.20, random_state=123)
    
    #Fit the model
    linreg = LinearRegression()
    model = linreg.fit(X_train, y_train)
    model
    
    #View model accuracy
    train_score = model.score(X_train, y_train)
    test_score = model.score(X_test, y_test)
print('Training Score:', round(train_score, 2))
    print('Test Score:', round(test_score, 2))
    print('Coefficients:', model.coef_)
    
    #View model summary in statsmodels
    X_train = sm.add_constant(X_train)
    smmodel = sm.OLS(y_train, X_train).fit()
    print(smmodel.summary())
    return smmodel

Модель 1. Использование всех функций

Моя первая модель имела скорректированный r-квадрат 0,736, но остатки не были нормальными, что нарушало одно из предположений линейной регрессии: нормальность. Хотя визуализация сама по себе субъективна, график квантиль-квантиль дал мне четкое указание на то, что распределение остатков не было нормальным.

plt.figure(figsize=(12, 8))
fig = sm.graphics.qqplot(model.resid, dist=stats.norm, line='45', fit=True)
plt.title('QQ Plot')
plt.show()

Модель 2. Нормализация зависимой переменной

Вместо того, чтобы регистрировать преобразование зависимой переменной, я хотел сосредоточиться на том, чтобы моя модель была как можно более интерпретируемой. В качестве альтернативы я снизил цену до 900 тысяч, чтобы нормализовать остатки. Следовательно, уменьшение нашей зависимой переменной снизило наш r-квадрат до 0,723, но наши остатки намного ближе к нормальному распределению.

Есть еще несколько несущественных p-значений выше 0,05, но прежде чем разбираться с ними, я сначала рассмотрел потенциальную мультиколлинеарность.

Модель 3. Работа с мультиколлинеарностью

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

Сначала я проверил потенциальную муликоллинеарность с помощью тепловой карты, чтобы увидеть, как можно коррелировать независимые переменные:

#View heatmap to get an idea of multicollinearity
X = df.drop(columns=['price'], axis=1)
plt.figure(figsize=(14, 10))
sns.heatmap(X.corr(), center=0)
plt.show()

Среди независимых переменных определенно существует мультиколлинеарность. Чтобы определить, какие переменные следует отбросить в результате мультиколлинеарности, я использовал коэффициент инфляции дисперсии (VIF) в качестве метрики. Коэффициент инфляции дисперсии измеряет степень мультиколлинеарности между независимыми переменными. Я создал словарь функций вместе с их VIF. Обратите внимание, что для вычисления VIF необходимо добавить константу / точку пересечения. Это легко сделать с помощью метода statsmodels add_constant.

from statsmodels.stats.outliers_influence import variance_inflation_factor
#Create dictionary of features and their variance inflation factor
X = sm.add_constant(X)
vif = [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]
vif_dict = dict(zip(X.columns, vif))
vif_dict

Используя отсечку VIF, равную 5, я удалил две функции.

#Create a list of columns to drop with a vif cutoff of 5
new_dict = {}
for (key, value) in vif_dict.items():
    if value >= 5:
        new_dict[key] = value
columns_to_drop = list(new_dict.keys())
columns_to_drop = columns_to_drop[1:]
columns_to_drop
Out: ['sqft_living', 'sqft_above', 'sqft_basement']

После удаления sqft_above и sqft_basement VIF для всех функций был меньше 5, а мультиколлинеарность уменьшилась. Я перестроил модель с новым фреймворком. Скорректированный R в квадрате остался прежним - 0,723, но столбец month_sold имел значение p, равное 0,38, и его необходимо было удалить.

Модель 4. Удаление незначительных элементов

После удаления month_sold скорректированный r-квадрат остался на уровне 0,723. Следующая модель касается уточнения функций путем удаления дополнительных выбросов.

Модель 5. Возможности уточнения

Моим первым шагом было изучение распределений для всех функций в данных.

#View distribution plots for all columns
for col in df.columns:
    plt.subplots(1, 1)
    sns.distplot(df[col])

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

df = df[df['bedrooms'] <= 11]
df = df[df['sqft_lot'] <= 15000]
df = df[df['sqft_living'] <= 5000]
df = df.drop(columns=['zipcode'], axis=1)
df = df.drop(columns=['floors'], axis=1)

После удаления выбросов и некоторых функций мой скорректированный r-квадрат теперь составляет 0,721. Между функциями, безусловно, существует взаимодействие, поэтому я создал условия взаимодействия в нашей следующей модели.

Модель 6. Работа с взаимодействиями

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

linreg = LinearRegression()
#Split features and target
X = df.drop(columns=['price'], axis=1)
y = df.price
#Utilize cross validation to determine baseline score
crossvalidation = KFold(n_splits=10, shuffle=True, random_state=1)
baseline = np.mean(cross_val_score(linreg, X, y, scoring='r2', cv=crossvalidation))
from itertools import combinations
interactions = []
feat_combinations = combinations(X.columns, 2)
data = X.copy()
for i, (a, b) in enumerate(feat_combinations):
    data['interaction'] = data[a] * data[b]
    score = np.mean(cross_val_score(linreg, data, y, scoring='r2', cv=crossvalidation))
    if score > baseline:
        interactions.append((a, b, round(score,3)))
    
    if i % 50 == 0:
        print(i)
            
print('Top 5 interactions: %s' %sorted(interactions, key=lambda inter: inter[2], reverse=True)[:10])

Out:
Top 5 interactions: [('lat', 'distance', 0.749), ('grade', 'distance', 0.728), ('sqft_living', 'distance', 0.726), ('sqft_living15', 'distance', 0.726), ('sqft_living', 'lat', 0.725), ('grade', 'lat', 0.725), ('lat', 'sqft_lot15', 0.724), ('sqft_lot', 'lat', 0.723), ('lat', 'sqft_living15', 0.723), ('long', 'distance', 0.722)]

Я перестроил модель с двумя условиями взаимодействия. Они создаются простым умножением двух взаимодействующих независимых переменных:

df[‘lat_distance’] = df[‘lat’] * df[‘distance’]
df[‘sqft_living15_distance’] = df[‘sqft_living15’] * df[‘distance’]

Моя новая модель имела скорректированный R-квадрат 0,757. Наряду с тем, что sqft_lot15 не имеет значения в результате этой модели, еще одна проблема заключалась в том, что спальни имели положительную корреляцию с ценой, но отрицательный коэффициент. Опять же, моей целью было сделать мою модель как можно более интерпретируемой, поэтому я решил отказаться от этой функции.

Модель 7: окончательная модель

После исключения спален и sqft_lot15 окончательная модель имела скорректированный r-квадрат 0,756, и все p-значения были значимыми. Ниже приводится краткое изложение модели.

Сводка модели

Множественный регрессионный анализ использовался, чтобы проверить, значительно ли определенные переменные предсказывали продажную цену домов в округе Кинг, штат Вашингтон. Результаты регрессии показали, что 14 предикторов и 2 условия взаимодействия объясняют 75,6% дисперсии (R2 = 0,756, F = 2622, p

Интерпретация коэффициентов

Распечатайте окончательную формулу регрессии:

print("Final Regression Formula\n")
print(model.intercept_, '+')
print('sum(')
for i in range(len(model_coefficients)):
    print(model_coefficients.coefficients[i], '*', model_coefficients.index[i])
print(')')
Out:
Final Regression Formula

-30602542.83254475 +
sum(
8875.896126406791 * bathrooms
76.48922676734944 * sqft_living
-1.9165517689290728 * sqft_lot
273549.5343291163 * waterfront
21735.09058989604 * view
21941.740311421723 * condition
59042.33314211221 * grade
890730.8205766763 * lat
414358.54325871647 * long
103.63861035136415 * sqft_living15
19158.636202298992 * year_sold
947.3916140076603 * age
43401.868300294445 * renovated
2087170.6107680988 * distance
-44069.613195667 * lat_distance
-3.8970828618299813 * sqft_living15_distance
)

В частности, глядя на 3 коэффициента и предиктора, мы можем сделать вывод из окончательной модели, что:

  • Дома, отремонтированные за последние 10 лет или построенные за последние 5 лет, увеличивают продажную цену дома на 43 401,87 доллара.
  • Увеличение площади внутренней жилой площади на единицу увеличивает продажную цену на 76,49 долларов. Увеличение площади на 1 тыс. Увеличивает продажную цену на 76 490 долларов.
  • Повышение качества строительства на одну единицу увеличивает продажную цену на 59 042,33 доллара.

Заключение

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

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