Комплексное исследование соревнований 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