Комплексное исследование соревнований Kaggle: https://www.kaggle.com/c/sf-crime
Постановка проблемы. Преступность — большая проблема в Сан-Франциско. Преступление подразделяется на множество категорий, включая нападение, кражу и т. д. Учитывая атрибуты конкретного преступления, такие как время, район и т. д., можем ли мы создать модель, чтобы предсказать, к какой категории относится это преступление?
Первым шагом является импорт всех необходимых библиотек, а также обучающих и тестовых наборов.
# This Python 3 environment comes with many helpful analytics libraries installed # It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python # For example, here's several helpful packages to load import numpy as np # linear algebra import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv) import sklearn import geopandas as gpd import seaborn as sns from sklearn import preprocessing from sklearn import utils from sklearn.ensemble import HistGradientBoostingClassifier from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline from sklearn.model_selection import GridSearchCV import geoplot as gplt from shapely.geometry import Point from sklearn.impute import SimpleImputer import shapely.wkt import contextily as ctx import matplotlib.pyplot as plt # Input data files are available in the read-only "../input/" directory # For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory from sklearn.ensemble import GradientBoostingClassifier import os for dirname, _, filenames in os.walk('/kaggle/input'): for filename in filenames: print(os.path.join(dirname, filename)) # You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using "Save & Run All" # You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session train = pd.read_csv('/kaggle/input/sf-crime/train.csv.zip') test = pd.read_csv('/kaggle/input/sf-crime/test.csv.zip')
Исследование данных:
1. Изучите набор данных и просмотрите функции
2. Обработайте выбросы и дубликаты
3. Оцените, какие из них следует продолжать, используя интуицию и визуализацию
train.head()
Функции и формат
Многие из этих записей не являются числами, а модели машинного обучения нуждаются в числах для функционирования, поэтому одной из наших задач будет кодирование этих функций в числа.
Даты — указанная дата совершения преступления в формате даты. Я планирую разделить это на несколько функций.
Категория — вид преступления
Дескрипт — описание преступления
DayOfWeek — самоочевидно
PdDistrict — район Сан-Франциско, в котором произошло преступление.
Разрешение — исход преступления (были ли арестованы)
Адрес — говорит сам за себя
X — долгота преступления
Y — широта преступления
Прежде чем кодировать их в нашу модель, давайте посмотрим, какие функции имеет набор тестовых данных.
test.head()
Какие функции отличаются?
В наборе тестов нет описания, категории или разрешения.
Категория — это наша целевая переменная, поэтому имеет смысл использовать переменную для обучения, а не для тестирования. Это мы и будем прогнозировать.
Давайте оставим разрешение и описание, поскольку ни одно из них недоступно в тестовом наборе.
Визуализация и интуиция
category_percents = train['Category'].value_counts()/ len(train['Category']) frame = dict(category_percents) result = pd.DataFrame(frame, index = [0]) result plt.tick_params(axis='both', which='major', labelsize=7) sns.barplot(result, orient ='h')
Как мы видим, воровство занимает первое место в списке с большим отрывом. Другие правонарушения и нападения неуголовного характера также находятся там. Далее давайте посмотрим, как преступность различается по районам.
district_crime_percents = train['PdDistrict'].value_counts()/ len(train['PdDistrict']) frame = dict(district_crime_percents) result = pd.DataFrame(frame, index = [0]) plt.tick_params(axis='both', which='major', labelsize=7) sns.barplot(result, orient ='h')
В Южном округе и в Миссионерском округе самая высокая доля преступлений, а это означает, что этот округ является довольно хорошим индикатором преступности. Давайте сделаем то же самое для дня недели.
DayOfWeek_crime_percents = train['DayOfWeek'].value_counts()/ len(train['DayOfWeek']) frame = dict(DayOfWeek_crime_percents) result = pd.DataFrame(frame, index = [0]) plt.tick_params(axis='both', which='major', labelsize=7) sns.barplot(result, orient ='v')
Не похоже, что дни недели оказывают такое большое влияние на преступность, поскольку они относительно равномерны.
Чтобы лучше визуализировать город НФ и соответствующие районы, давайте создадим визуальное представление со всеми аннотированными районами.
sf_ = gpd.read_file('/kaggle/input/san-francisco-neighborhood-maps/planning_neighborhoods.shx') sf_['coords'] = sf_['geometry'].apply(lambda x: x.representative_point().coords[:]) sf_['coords'] = [coords[0] for coords in sf_['coords']] fig, ax = plt.subplots(figsize=(10,10)) # Plot our SF GeoDataFrame for idx,row in sf_.iterrows(): plt.annotate(text=row['neighborho'], xy= row['coords'], horizontalalignment='center', fontsize = 7) sf_.plot(ax=ax, alpha = 1)
Немного поясним код: GPD — это экземпляр geopandas, проекта с открытым исходным кодом для работы с пространственными данными. GPD принимает файл формы, который представляет собой список геометрий, которые будут соединяться в форму (в данном случае форму окрестностей научной фантастики).
Вторая строка кода создает репрезентативную точку где-то в каждом районе, которую я затем использую позже для аннотирования имени каждого района.
Выбросы
Давайте посмотрим на выбросы в зависимости от местоположения.
Широта 37°45′25,20″ северной широты и 122°26′56,40″ западной долготы — это соответствующие широта и долгота научной фантастики, поэтому давайте посмотрим и изменим те, которые находятся за пределами этих границ.
train['Y'].describe()
train['X'].describe()
Мы можем заметить, что все долготы (X) находятся в пределах, а широты (Y) — нет. Мы можем это сказать, потому что максимум — 90, что даже близко не соответствует SF. Давайте воспользуемся импьютером (модулем, который заменяет нулевые значения на основе различных стратегий), чтобы взять среднее значение тех широт, которые выходят за пределы, и заменить их средней широтой соответствующего района PD.
results = train.loc[train['Y']>40] results
#drop duplicates, replace outliers with null values train.drop_duplicates(inplace=True) train.replace({'X': -120.5, 'Y': 90.0}, np.NaN, inplace=True) test.replace({'X': -120.5, 'Y': 90.0}, np.NaN, inplace=True) #Fill missing values in 'X' and 'Y' columns with the mean values of their respective districts train[['X', 'Y']] = train.groupby('PdDistrict')[['X', 'Y']].transform(lambda x: x.fillna(x.mean())) test[['X', 'Y']] = test.groupby('PdDistrict')[['X', 'Y']].transform(lambda x: x.fillna(x.mean()))
Далее я хочу определить функции предварительной обработки, чтобы принимать нужные мне аспекты даты (год, месяц, день, час).
#take out all non-numeric values in Date def convertDate(date): ans='' for char in str(date): if char.isnumeric(): ans+=char return ans #convert to string def fullDate(date): return str(date) def retrieveYear(date): ans= date[:4] return int(ans) def retrieveMonth(date): ans = date[4:6] return int(ans) def retrieveDay(date): ans = int(date[6:8]) return ans % 7 def retrieveHour(date): ans = date[8:10] return int(ans) # applying functions to both training and test set train['Dates'] = train['Dates'].apply(convertDate) train['fullDate'] = train['Dates'].apply(fullDate) train['Year']=train['fullDate'].apply(retrieveYear) train['Month']=train['fullDate'].apply(retrieveMonth) train['Day']=train['fullDate'].apply(retrieveDay) train['Hour']=train['fullDate'].apply(retrieveHour) test['Dates'] = test['Dates'].apply(convertDate) test['fullDate'] = test['Dates'].apply(fullDate) test['Year']=test['fullDate'].apply(retrieveYear) test['Month']=test['fullDate'].apply(retrieveMonth) test['Day']=test['fullDate'].apply(retrieveDay) test['Hour']=test['fullDate'].apply(retrieveHour) #dropping unnecessary features from train and test. #for test we will rejoin id after training model into final dataframe train = train.drop(labels = 'Descript', axis =1) train = train.drop(labels = 'Resolution', axis =1) ids = test['Id'] test = test.drop(labels ='Id', axis =1)
Кодирование
Мы используем кодировщик меток, который будет принимать каждый класс значений и присваивать номер этому классу. В следующем коде я просматриваю и кодирую все значения всех столбцов как в поезде, так и в тесте.
#encoding all the features in both and train and test lab_enc = preprocessing.LabelEncoder() for col in train: train[col] = lab_enc.fit_transform(train[col]) for col in test: test[col] = lab_enc.fit_transform(test[col]) #dropping category from train set, so I can successfully use train, split Skikit Y = train['Category'] train= train.drop(labels = 'Category', axis =1) #preparing row names and columns for later when we are joining probabilistic predictions with columns row_names = ids column_names = np.array(['ARSON', 'ASSAULT', 'BAD CHECKS', 'BRIBERY', 'BURGLARY', 'DISORDERLY CONDUCT', 'DRIVING UNDER THE INFLUENCE', 'DRUG/NARCOTIC', 'DRUNKENNESS', 'EMBEZZLEMENT', 'EXTORTION', 'FAMILY OFFENSES', 'FORGERY/COUNTERFEITING', 'FRAUD', 'GAMBLING', 'KIDNAPPING', 'LARCENY/THEFT', 'LIQUOR LAWS', 'LOITERING', 'MISSING PERSON', 'NON-CRIMINAL', 'OTHER OFFENSES', 'PORNOGRAPHY/OBSCENE MAT', 'PROSTITUTION', 'RECOVERED VEHICLE', 'ROBBERY', 'RUNAWAY', 'SECONDARY CODES', 'SEX OFFENSES FORCIBLE', 'SEX OFFENSES NON FORCIBLE', 'STOLEN PROPERTY', 'SUICIDE', 'SUSPICIOUS OCC', 'TREA', 'TRESPASS', 'VANDALISM', 'VEHICLE THEFT', 'WARRANTS', 'WEAPON LAWS'])
Здесь следует иметь в виду, что после кодирования всех значений в нашей категории они будут числами, а не именами, которые мы видим выше. SkiKit Label Encoder имеет обратное сопоставление, но здесь я отсортировал имена в алфавитном порядке, поскольку именно в этом порядке кодировщик следует, чтобы выровнять наши прогнозы.
Наш последний фрейм данных, который мы будем отправлять, будет фреймом данных с заголовком. Каждая строка будет иметь идентификатор и соответствующие вероятности каждой категории преступлений. Имена столбцов представляют собой список всех категорий в алфавитном порядке. Имена строк являются идентификаторами.
X_train, X_test, y_train, y_test = train_test_split(train,Y,random_state=0) pipe = Pipeline([('HistGradientBoosting',HistGradientBoostingClassifier ())]) pipe.fit(X_train, y_train) score = pipe.score(X_test, y_test) print(score)
Это наша базовая модель, и наш балл составляет 0,25. Что это значит? Наша оценка основана на потере журнала, которая выглядит следующим образом:
Я не буду вдаваться в подробности, как работает эта формула, но если наши потери в журнале равны 0, это означает, что все наши прогнозы идеальны, поэтому наша оценка 0,25 неплохая.
Далее давайте посмотрим, как это оптимизировать.
pipe.get_params()
Примечания к классификатору:
HistGradient очень эффективен и требует гораздо меньше времени для оптимизации, чем случайный лес или повышение градиента.
Для оптимизации я собираюсь использовать GridSearchCV. GridSearchCV требует словаря параметров, а также пространства поиска для этих параметров (возможных значений). Затем GridSearchCV предоставит нам комбинацию этих параметров с наименьшими потерями.
Я решил оптимизировать как max_leaf_nodes, так и min_samples_leaf.
Max_leaf_nodes — это способ контролировать глубину дерева, ограничивая возможное количество листьев. Если листьев слишком много, мы склонны к переоснащению, но верно и обратное.
Min_samples_leaf будет контролировать минимальное количество выборок, прежде чем его можно будет считать листом.
alg = HistGradientBoostingClassifier (warm_start=True) params = {'max_leaf_nodes':range(30,71,10), 'min_samples_leaf':[10,20,30]} clf = GridSearchCV(alg, params, n_jobs =3, cv =5, scoring = 'neg_log_loss') clf.fit(X_train, y_train) submission_predictions = clf.predict_proba(test) res = pd.DataFrame(submission_predictions, index=row_names, columns=column_names) res.to_csv('out.csv')
n_jobs относится к распараллеливанию. В моем случае я могу использовать максимум три процессора одновременно, поэтому я установил значение 3. Ниже показано, как выглядит фрагмент представленного материала.
Вот и все, надеюсь, вам понравилось!
Ссылка на Github: https://github.com/lokavarapumadhukar/SFCrimeExploration