Введение:
Быстрый рост технологических гигантов, таких как Tesla и Amazon, значительно повлиял на рынок жилья Техаса. По мере того, как эти компании строят свои мегафабрики и центры выполнения заказов, спрос на жилье резко вырос, затронув местных жителей Техаса. В этой статье для начинающих мы рассмотрим основы машинного обучения и программирования на Python для анализа рынка жилья Техаса. Мы будем использовать данные о недвижимости и алгоритм кластеризации K-средних, чтобы выявить закономерности и тенденции на рынке. В частности, мы проанализируем расстояние от списка жилых домов Zillow до завода Tesla Giga, среднее расстояние до станций наддува Telsa и, наконец, среднее расстояние до завода Amazon Fulfillment. Никаких продвинутых знаний в области кодирования или искусственного интеллекта не требуется, только базовый интерес к STEM!
ВВЕДЕНИЕ. Что такое алгоритм кластеризации K-средних?
Кластеризация K-средних — это популярный алгоритм кластеризации, относящийся к классу Обучение без учителя машинного обучения. Алгоритм работает путем разделения данных на числовые кластеры k, где каждый кластер представлен своим центром тяжести. Кластерные назначения каждой точки данных (в данном случае квартиры) и центроидов (случайно выбранная квартира, которая является базовым дескриптором изучаемых групп) итеративно уточняются и перемещаются до сходимости.
Пользователи должны заранее указать количество кластеров (k). K-means предполагает, что кластеры имеют сферическую форму и одинаковые размеры, что не всегда верно для реальных данных. Этот алгоритм чувствителен к шуму и выбросам, поскольку они могут существенно повлиять на центроиды кластера.
Однако k-means можно масштабировать до больших наборов данных, особенно при использовании оптимизированных реализаций, таких как MiniBatchKMeans. Таким образом, k-means — это простой и быстрый алгоритм кластеризации, который хорошо работает для данных со сферическими кластерами одинакового размера, но может не работать с более сложными данными.
Шаг 1: Сбор данных о недвижимости
Мы рассмотрим два метода сортировки данных о жилье с помощью алгоритма кластеризации K-средних.
- Общедоступный Неофициальный API Zillow RapidAPI (бесплатный, но с ограничениями по объему запросов, чистоте данных и возможности фильтрации)
- Общедоступный набор данных Kaggle чистых и отформатированных данных из списков Zillow в районе метро Остина, который обычно используется в исследованиях и разработках.
Вы выбираете, какой способ лучше для вас, RapidAPI может быть хорошей практикой для выполнения вызовов API и обработки файлов .json. В то время как набор данных Kaggle научит вас манипулировать файлами .csv.
import requests import csv import json def search_zillow_api(location): """Search Zillow API for current listings in the specified location.""" # API endpoint url = "https://zillow56.p.rapidapi.com/search" querystring = {"location": location, "rentzestimate": "true", "bedrooms": "1+", "bathrooms": "1+", "squareFeet": "1+", "price": "1+", "latitude": "0+", "livingArea": "0+", "latitude": {"$gt": 0}, "lotAreaValue": {"$gt": 100}} headers = { "X-RapidAPI-Key": "YOUR API KEY", "X-RapidAPI-Host": "zillow56.p.rapidapi.com" } response = requests.request("GET", url, headers=headers, params=querystring) # Check if the API call was successful if response.status_code == 200: print(response.text) else: print("API call failed with status code:", response.status_code) return response if __name__ == '__main__': # Opening JSON file _TX_ZILLOW = "/in_Data/atx_zillow/austinHousingData.csv" TX_ZILLOW = [] with open(_TX_ZILLOW) as file: csvreader = csv.reader(file) TX_ZILLOW_header = next(csvreader) for row in csvreader: TX_ZILLOW.append(row) # ZILLOW API ############################## Search Zillow API for current listings response = search_zillow_api('austin, texas') z_json = response.json() # Serializing json json_object = json.dumps(z_json, indent=4) # Writing to sample.json with open("/in_Data/zillow_texas_json/sample01.json", "w") as outfile: outfile.write(json_object) # ///// ZILLOW API ############################## Search Zillow API for current listings
Шаг 2: Загрузка данных и расчет расстояний
Прежде чем погрузиться в k-средние, нам нужно предварительно обработать данные, чтобы сделать их пригодными для анализа. Этот шаг включает в себя очистку и форматирование данных (в случае данных Zillow Api), а также стандартизацию значений.
Начнем с очистки и стандартизации наших входных данных. Если вы используете метод API Zillow, вы заметите, что не каждая запись имеет долготу и широту, или может отсутствовать площадь в квадратных футах и т. д. В этих случаях отсутствия геолокации мы можем вызвать Google Maps Api для расчета расстояния между списками квартир и одним из заводов или станций Super Charger.
ПРИМЕЧАНИЕ! API карт Google не является бесплатным, но стоимость этого анализа практически незначительна. Обязательно сохраните выходной файл, а затем закомментируйте этот вызов, чтобы не начислять слишком много комиссий при последующем тестировании и отладке.
import requests import csv import json def load_and_filter_json(file_path): with open(file_path, 'r') as json_file: data = json.load(json_file) filtered_data = [] required_keys = ['bathrooms', 'bedrooms', 'price', 'latitude', 'longitude'] for obj in data['results']: if all(key in obj for key in required_keys) and ('lotAreaUnit' in obj or 'livingArea' in obj): if all(int(obj[val]) != 0 for val in required_keys) and (obj not in filtered_data): filtered_data.append(obj) # Serializing json json_object = json.dumps(filtered_data, indent=4) # Writing to sample.json with open("filtered01.json", "w") as outfile: outfile.write(json_object) return filtered_data def get_geocode(address): """ Uses Google Maps API to get the geocode of a given address. Args: address (str): The address to get the geocode for. Returns: A tuple of latitude and longitude. """ # # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # # https: // developers.google.com / maps / billing - and -pricing / pricing # distance-matrix # # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # # ' https://www.google.com/maps/embed/v1/MAP_MODE?key=AIzaSyBj2td7I4g-fF9ZV_GqrNALth0_ESoKcTA' GOOGLE_MAPS_API_URL = 'https://maps.googleapis.com/maps/api/geocode/json' params = { 'address': address, 'sensor': 'false', 'region': 'texas', 'key': 'YOUR API KEY' } # Do the request and get the response data req = requests.get(GOOGLE_MAPS_API_URL, params=params) res = req.json() # Use the first result try: lat_lng = res['results'][0]['geometry']['location'] return (lat_lng['lat'], lat_lng['lng']) except: print('Error: could not find geocode for address:', address) return None def get_distances(tx_zillow, tx_Amzn, tesla_sc): # Gigafactory Texas tgf_geoloc = (30.22, -97.62) sc_dist_avg = [] fc_dist_avg = [] tgf_dist_km = [] house_ids = [] for j in tx_zillow: try: z_lat = j[5] z_long = j[6] except: print('No Geo data in Zillow datapoint') fc_z_dist_km = [] for i in tx_Amzn: fc_z_geodist = geodesic((z_lat, z_long), (tx_Amzn[i][0], tx_Amzn[i][1])).km fc_z_dist_km.append(fc_z_geodist) sc_z_dist_km = [] for i in tesla_sc: sc_z_geodist = geodesic((z_lat, z_long), (i[8][0], i[8][0])).km sc_z_dist_km.append(sc_z_geodist) tgf_dist_km.append(geodesic((z_lat, z_long), (tgf_geoloc[0], tgf_geoloc[1])).km) sc_dist_avg.append(np.average(sc_z_dist_km)) fc_dist_avg.append(np.average(fc_z_dist_km)) house_ids.append(j[0]) tgf_dist_from_zillow = [] sc_dist_from_zillow = [] fc_dist_from_zillow = [] for i_tfg in tgf_dist_km: tgf_dist_from_zillow.append(i_tfg) for i_sc in sc_dist_avg: sc_dist_from_zillow.append(i_sc) for i_fc in fc_dist_avg: fc_dist_from_zillow.append(i_fc) return tgf_dist_from_zillow, sc_dist_from_zillow, fc_dist_from_zillow
Добавив эти два метода в наш базовый код, мы теперь можем просмотреть данные о квартире и вычислить расстояния, необходимые для алгоритма кластеризации k-средних. Мы будем сохранять результаты локально по мере продвижения. И я уже проделал кропотливую работу по поиску геолокации гигафабрики Tesla, Tesla Super Charger и Amazon Fulfillment.
Функция get_distances принимает tx_zillow, tx_Amzn и tesla_sc в качестве входных данных, представляющих данные о квартирах, центрах выполнения заказов Amazon и местоположениях Tesla Super Charger соответственно.
Функция рассчитывает среднее расстояние от каждой квартиры до центров Tesla Gigafactory, Tesla Super Charger и Amazon Fulfillment с использованием геодезического расстояния. Затем эти расстояния добавляются в соответствующие списки. Кроме того, идентификатор каждого дома также хранится в отдельном списке.
Функция возвращает три списка: tgf_dist_from_zillow, sc_dist_from_zillow и fc_dist_from_zillow, в которых хранятся расстояния от квартир до центров Gigafactory, Super Charger и Fulfillment соответственно. Вычисленные расстояния будут использоваться в качестве входных признаков для алгоритма кластеризации k-средних на следующем шаге.
from geopy.distance import geodesic if __name__ == '__main__': # Opening JSON file # _TX_ZILLOW = "/in_Data/zillow_texas_json/sample01.json" _TX_ZILLOW = "/in_Data/atx_zillow/austinHousingData.csv" _TX_AMZN = "/in_Data/amazon_fullfillment_centers.csv" _TESLA_SC = "/in_Data/texas_tesla_superchargers.csv" _FC_GEO_LOC = "in_Data/fc_geo_dict.json" TX_ZILLOW = [] with open(_TX_ZILLOW) as file: csvreader = csv.reader(file) TX_ZILLOW_header = next(csvreader) for row in csvreader: TX_ZILLOW.append(row) TX_AMZN = [] with open(_TX_AMZN) as file: csvreader = csv.reader(file) TX_AMZN_header = next(csvreader) for row in csvreader: TX_AMZN.append(row) TESLA_SC = [] with open(_TESLA_SC) as file: csvreader = csv.reader(file) TESLA_SC_header = next(csvreader) for row in csvreader: TESLA_SC.append(row) FC_GEO_LOC = [] with open(_FC_GEO_LOC) as file: csvreader = csv.reader(file) FC_GEO_LOC_header = next(csvreader) for row in csvreader: FC_GEO_LOC.append(row) # i = "/in_Data/zillow_texas_json/sample01.json" # z_json = load_and_filter_json(i) # ZILLOW API ############################## Search Zillow API for current listings # response = search_zillow_api('austin, texas') # z_json = response.json() # # Serializing json # json_object = json.dumps(z_json, indent=4) # # Writing to sample.json # with open("/in_Data/zillow_texas_json/sample01.json", "w") as outfile: # outfile.write(json_object) # //// ZILLOW API ############################## # Google Maps API ############################### FC_GEO_DICT = dict() # initialize an empty dictionary for fc in TX_AMZN: addr = get_geocode(fc[1] + ', ' + fc[2] + ', ' + fc[3] + ', ' + fc[4]) FC_GEO_DICT.update({fc[0]: addr}) #out_file = open("fc_geo_dict.json", "w") out_file = open("fc_geo_dict.json", "w") json.dump(FC_GEO_DICT, out_file, indent=4) out_file.close() SC_GEO_DICT = dict() # initialize an empty dictionary for sc in TESLA_SC: addr = get_geocode(fc[1] + ', ' + fc[2] + ', ' + fc[3] + ', ' + fc[4]) SC_GEO_DICT.update({TESLA_SC[sc][0]: (addr)}) out_file = open("fc_geo_dict.json", "w") json.dump(SC_GEO_DICT, out_file, indent=4) out_file.close() tgf_dist_from_zillow, sc_dist_from_zillow, fc_dist_from_zillow = get_distances(TX_ZILLOW, FC_GEO_LOC, TESLA_SC) # creating json files out_file = open("tx_sc_dist_from_zillow.json", "w") json.dump(sc_dist_from_zillow, out_file, indent=4) out_file.close() out_file = open("tx_fc_dist_from_zillow.json", "w") json.dump(fc_dist_from_zillow, out_file, indent=4) out_file.close() out_file = open("tx_tgf_dist_from_zillow.json", "w") json.dump(tgf_dist_from_zillow, out_file, indent=4) out_file.close() # ///// Google Maps API ##############################
Шаг 3. Тензорный стек
Теперь, когда у нас есть все движущиеся части, давайте загрузим данные в тензорный стек, чтобы алгоритм обработал их в кластеры.
import torch from datetime import datetime, date import json def normalize_dates(date, start_date, scaling_factor): """ Normalize date data based on a specific start date. Args: dates (list): A list of date strings in the format 'YYYY-MM-DD'. start_date (str): The reference start date string in the format 'YYYY-MM-DD'. scaling_factor (int): The number of days to normalize the range of values. Returns: list: A list of normalized date values. """ # for date in dates: date_dt = datetime.strptime(date, '%Y-%m-%d').date() days_diff = (date_dt - start_date).days normalized_date = days_diff / scaling_factor return normalized_date #....... if __name__ == '__main__': _tgf_dist_from_z = '/in_Data/self_made/tx_tgf_dist_from_zillow.json' _fc_dist_from_z = '/in_Data/self_made/tx_fc_dist_from_zillow.json' _sc_dist_from_z = '/in_Data/self_made/tx_sc_dist_from_zillow.json' tgf_f = open(_tgf_dist_from_z) tgf_dist_from_z = json.load(tgf_f) tgf_f.close() FC_f = open(_fc_dist_from_z) fc_dist_from_z = json.load(FC_f) FC_f.close() SC_f = open(_sc_dist_from_z) sc_dist_from_z = json.load(SC_f) SC_f.close() # Load housing data data = {"tgf": [], "sc": [], "fc": [], "bathrooms": [], "bedrooms": [], "price": [], "sqft": [], "avgSchoolRating": [], "latest_saledate": [], "year_built": []} # atx_gf_date = date(2020, 7, 22) atx_gf_date = datetime.strptime('2020-07-22', '%Y-%m-%d').date() scaling_factor = 30 for zj in TX_ZILLOW: data["bedrooms"].append(int(zj[44])) data["bathrooms"].append(float(zj[43])) data["price"].append(float(zj[18])) data["sqft"].append(float(zj[34])) data["avgSchoolRating"].append(float(zj[40])) date_object = normalize_dates(zj[20], atx_gf_date, scaling_factor) data["latest_saledate"].append(date_object) year_built_date = datetime.strptime(zj[17], '%Y').date() year_object = normalize_dates(str(year_built_date), atx_gf_date, scaling_factor) data["year_built"].append(year_object) # json_converter.flatten_list( data["tgf"] = tgf_dist_from_z data["sc"] = sc_dist_from_z data["fc"] = fc_dist_from_z tgf = torch.tensor(data['tgf'], dtype=torch.float32) sc = torch.tensor(data['sc'], dtype=torch.float32) fc = torch.tensor(data['fc'], dtype=torch.float32) bathrooms = torch.tensor(data['bathrooms'], dtype=torch.float32) bedrooms = torch.tensor(data['bedrooms'], dtype=torch.float32) price = torch.tensor(data['price'], dtype=torch.float32) sqft = torch.tensor(data['sqft'], dtype=torch.float32) avgSchoolRating = torch.tensor(data['avgSchoolRating'], dtype=torch.float32) latest_saledate = torch.tensor(data['latest_saledate'], dtype=torch.float32) year_built = torch.tensor(data['year_built'], dtype=torch.float32) ## Load feature stack tensors features = torch.stack([tgf, sc, fc, bathrooms, bedrooms, price, sqft, avgSchoolRating, latest_saledate, year_built], dim=1)
Этот шаг может показаться очень простым, но он может стать причиной отказа вашего кода, поэтому обязательно разберитесь с ним. В этом фрагменте кода различные данные о жилье из Zillow загружаются из файлов JSON, которые мы сохранили ранее, и объединяются в один словарь под названием data. В словаре хранятся характеристики жилья, такие как количество спален, ванных комнат, цена и площадь. Затем код преобразует каждый список функций в тензор PyTorch и складывает эти тензоры по второму измерению (столбцам) для создания тензора функций.
!Важно! Этот тензор признаков должен иметь одинаковую форму для всех признаков, потому что алгоритм кластеризации k-средних предполагает согласованный входной формат. В частности, каждая строка тензора должна представлять точку данных с одинаковым количеством признаков. Такое единообразие позволяет алгоритму вычислять расстояния и осмысленно назначать точки данных кластерам!
Также обратите внимание, что мы преобразовали (векторизовали) last_saledate и year_built в числовые значения, представляющие количество дней с 22 июля 2020 года, дня открытия Tesla GigaFactory.
Шаг 4. Кластеризация методом К-средних
Теперь мы готовы запустить наш первый алгоритм машинного обучения!
import time import torch from pykeops.torch import LazyTensor from tqdm import tqdm def KMeans(x, K=10, Niter=10, verbose=True): """Implements Lloyd's algorithm for the Euclidean metric.""" start = time.time() N, D = x.shape # Number of samples, dimension of the ambient space c = x[:K, :].clone() # Simplistic initialization for the centroids x_i = LazyTensor(x.view(N, 1, D)) # (N, 1, D) samples c_j = LazyTensor(c.view(1, K, D)) # (1, K, D) centroids # K-means loop: # - x is the (N, D) point cloud, # - cl is the (N,) vector of class labels # - c is the (K, D) cloud of cluster centroids for i in tqdm(range(Niter), desc="K-Means Iterations"): # for i in range(Niter): # E step: assign points to the closest cluster ------------------------- D_ij = ((x_i - c_j) ** 2).sum(-1) # (N, K) symbolic squared distances cl = D_ij.argmin(dim=1).long().view(-1) # Points -> Nearest cluster # M step: update the centroids to the normalized cluster average: ------ # Compute the sum of points per cluster: c.zero_() c.scatter_add_(0, cl[:, None].repeat(1, D), x) # Divide by the number of points per cluster: Ncl = torch.bincount(cl, minlength=K).type_as(c).view(K, 1) c /= Ncl # in-place division to compute the average if verbose: # Fancy display ----------------------------------------------- end = time.time() print( f"K-means for the Euclidean metric with {N:,} points in dimension {D:,}, K = {K:,}:" ) print( "Timing for {} iterations: {:.5f}s = {} x {:.5f}s\n".format( Niter, end - start, Niter, (end - start) / Niter ) ) return cl, l
Это готовый алгоритм кластеризации, который должен работать как есть. Обязательно прочитайте примечания и посмотрите, как они согласуются с этим псевдокодом:
Заключительный шаг: давайте графически!
import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D if __name__ == '__main__': n_clusters = 3 cl, c = KMeans(features, K=n_clusters, Niter=10, verbose=True) target_length = len(data) # Separate the data points into different clusters clusters = [[] for _ in range(len(c))] for i, point in enumerate(features): cluster_idx = cl[i] clusters[cluster_idx].append(point.numpy()) # Plot the clusters using different colors colors = ['r', 'g', 'b', 'c', 'm', 'y', 'k', 'w'] for i, cluster in enumerate(clusters): if len(cluster) > 0: cluster = np.stack(cluster) plt.scatter(cluster[:, 0], cluster[:, 9], color=colors[i % len(colors)], label=f'Cluster {i}') # Plot the cluster centers plt.scatter(c[:, 0], c[:, 9], color='black', marker='x', s=200, linewidths=3, label='Cluster centers') plt.xlabel('Distance to TGF') plt.ylabel('Year Built') plt.legend() plt.title('KMeans Clustering test (# clusters = {}, min_samples = {})'.format(n_clusters, target_length)) plt.savefig('KMeans Clustering test (# clusters = {}, min_samples = {}).png'.format(n_clusters, target_length)) plt.show() plt.close() # Plot the clusters using different colors colors = ['r', 'g', 'b', 'c', 'm', 'y', 'k', 'w'] for i, cluster in enumerate(clusters): if len(cluster) > 0: cluster = np.stack(cluster) plt.scatter(cluster[:, 0], cluster[:, 5], color=colors[i % len(colors)], label=f'Cluster {i}') # Plot the cluster centers plt.scatter(c[:, 0], c[:, 5], color='black', marker='x', s=200, linewidths=3, label='Cluster centers') plt.xlabel('Distance to TGF') plt.ylabel('Price') plt.legend() plt.title('KMeans Clustering test (# clusters = {}, # of DPs = {})'.format(n_clusters, target_length)) plt.savefig('tgf_latest_saledate_KMeans Clustering test (# clusters = {}, # of DPs = {}).png'.format(n_clusters, target_length)) plt.show() plt.close() # Create a 3D scatter plot fig = plt.figure() ax = fig.add_subplot(111, projection='3d') # Plot the clusters using different colors for i, cluster in enumerate(clusters): if len(cluster) > 0: cluster = np.stack(cluster) ax.scatter(cluster[:, 0], cluster[:, 4], cluster[:, 5], color=colors[i % len(colors)], label=f'Cluster {i}') # Plot the cluster centers ax.scatter(c[:, 0], c[:, 4], c[:, 5], color='black', marker='x', s=200, linewidths=3, label='Cluster centers') ax.set_xlabel('Distance to TGF') ax.set_ylabel('Number of Bedrooms') ax.set_zlabel('Price') ax.legend() ax.view_init(30, 45) # You can adjust these values to change the viewing angle plt.title('KMeans Clustering 3D test (# clusters = {}, (# of DPs = {})'.format(n_clusters, target_length)) plt.savefig('tgf_bedrooms_price_KMeans Clustering 3D test (n_clusters = {})'.format(n_clusters, target_length)) plt.show() plt.close() # Convert the features tensor to a NumPy array features_np = features.numpy() # Add the cluster labels as a new column data_with_labels = np.column_stack((features_np, cl.numpy())) # Create a pandas DataFrame with the updated data columns = ["tgf", "sc", "fc", "bathrooms", "bedrooms", "price", "sqft", "avgSchoolRating", "latest_saledate","year_built", "cluster"] df = pd.DataFrame(data_with_labels, columns=columns) # Save the DataFrame as a CSV file df.to_csv("housing_data_with_clusters.csv", index=False)
В этом фрагменте кода результаты кластеризации k-средних визуализируются с использованием различных графиков. Сначала точки данных разделяются на соответствующие кластеры, а затем создаются 2D-графики рассеяния для визуализации взаимосвязи между «расстоянием до TGF» и «годом постройки», а также между «расстоянием до TGF» и «ценой». Затем создается трехмерная точечная диаграмма, чтобы визуализировать взаимосвязь между «расстоянием до TGF», «количеством спален» и «ценой». На всех графиках каждый кластер представлен своим цветом, а центры кластеров отмечены черными крестиками.
Вывод должен выглядеть примерно так:
После визуализации результатов код преобразует тензор признаков в массив NumPy и добавляет метки кластера в качестве нового столбца. Затем обновленные данные преобразуются в кадр данных pandas и сохраняются в виде файла CSV.
Весь проект можно снести сюда:
https://github.com/dvrk-dvys/medium
Спасибо, что прочитали первую часть этого руководства~! Во второй части мы рассмотрим больше визуализаций, как интерпретировать эти результаты в контексте недвижимости ATX, как оптимизировать данные перед применением алгоритма k-средних, а также обсудим альтернативные алгоритмы кластеризации, которые устраняют ограничения k-средних, такие как как чувствительность к начальным условиям, обработка несферических кластеров и масштабируемость в многомерных пространствах. Оставайтесь с нами, чтобы узнать больше об основах машинного обучения и их применении.