Product Classification API Part 1: Data Acquisition
Юджин Ян описывает первую часть серии о создании API для классификации товаров: по названию товара API возвращает 3 наиболее вероятные категории (из тысяч). Вместо написания собственного веб-скрапера для Amazon и Alibaba автор использовал открытые данные о товарах Amazon, собранные Julian McAuley (метаданные на 9,4 млн товаров, 3,1 ГБ в архиве). В статье показано, как прочитать сжатый JSON в pandas и конвертировать в CSV, а также как обработать поле categories, превратив список списков в строку-путь категории (например, «Clothing, Shoes & Jewelry -> Men -> Shoes -> Fashion Sneakers»). Затем данные последовательно очищаются: исключаются товары без названия или категории (остаётся 7,98 млн), категории вроде Books/CDs & Vinyl/Movies & TV (5,59 млн), неглубочайшие категории (4,61 млн) и категории с менее чем 10 товарами (4,59 млн). В итоге из 17,6 тыс. категорий остаётся около 15 тыс., и эти подготовленные данные станут основой для обучения классификатора в следующих частях.
Product Classification API Part 1: Data Acquisition
Product Classification API. Часть 1: получение данных
[ machinelearning python 🛠 ] · чтение на 9 мин
To gain practice with building data products end-to-end, I recently developed a product classification API. The API helps classify products based on its title—instead of figuring out which category your product belongs to (out of thousands), you can provide the title and the API returns the top 3 most likely categories. (Github repositiory)
Чтобы попрактиковаться в создании дата-продуктов от начала до конца, я недавно разработал API для классификации товаров. Этот API помогает классифицировать товары по их названию — вместо того чтобы выяснять, к какой из тысяч категорий принадлежит ваш товар, вы можете передать название, и API вернёт 3 наиболее вероятные категории. (Репозиторий на Github)
Update: API discontinued to save on cloud cost.
Обновление: API остановлен ради экономии на облачных расходах.
Input: Title. Output: Suggested categories.
Вход: название. Выход: предлагаемые категории.
This is part of a series of posts on building a product classification API:
Это часть серии постов о создании API для классификации товаров:
Получение и форматирование данных (часть 1) Очистка и подготовка данных (часть 2) Разработка приложения (часть 3) Демо классификации изображений Демо поиска по изображениям
Where did I get the product data from?
Откуда я взял данные о товарах?
I initially intended to build a web scraper to collect product data from Amazon’s and Alibaba’s sites. However, I figured this (skill) was not absolutely necessary, especially at work—product data would be stored and available in databases. In addition, the process of scraping and structuring scraped data was estimated to take up at least 30% of overall effort. Thus, I decided to use open-sourced product data instead.
Изначально я собирался написать веб-скрапер, чтобы собрать данные о товарах с сайтов Amazon и Alibaba. Однако я решил, что этот навык не так уж необходим, особенно в работе — данные о товарах хранятся и доступны в базах данных. К тому же процесс скрапинга и структурирования собранных данных, по оценке, занял бы как минимум 30% всех усилий. Поэтому я решил использовать открытые данные о товарах.
It was surprisingly difficult to find good quality open-sourced product data. I was on the verge of building a scraper when I stumbled upon Julian McAuley’s site: http://jmcauley.ucsd.edu/data/amazon/. Acknowledgements to Julian and his team for the Amazon product data used to build this API.
На удивление трудно оказалось найти качественные открытые данные о товарах. Я уже был готов написать скрапер, когда наткнулся на сайт Julian McAuley: http://jmcauley.ucsd.edu/data/amazon/. Выражаю благодарность Julian и его команде за данные о товарах Amazon, использованные при создании этого API.
For this project, we’ll be using the product metadata, containing 9.4 million products (3.1gb zipped). As we’ll see, after cleaning and preparation, only a fraction is usable for training a product classification model for the API.
В этом проекте мы будем использовать метаданные товаров, содержащие 9,4 млн товаров (3,1 ГБ в архиве). Как мы увидим, после очистки и подготовки лишь небольшая часть пригодна для обучения модели классификации товаров для API.
How does the product metadata look like?
Как выглядят метаданные товаров?
The metadata contains the following fields:
Метаданные содержат следующие поля:
asin: ID товара title: название товара price: цена товара (в USD) imUrl: URL изображения товара related: связанные товары (которые рекомендует Amazon; например, «также покупали», «также смотрели», «покупали вместе» и т. д.) salesRank: ранг товара в категории верхнего уровня (по количеству продаж за период) brand: бренд товара categories: список категорий, к которым относится товар
Here’s how a product would look like in the original json format. I’ll be using this example throughout this post.
Вот как выглядел бы товар в исходном формате json. Я буду использовать этот пример на протяжении всего поста.
{
"asin": "B0147ZZKQ2",
"title": "Onitsuka Tiger Ultimate 81 Running Shoe",
"price": 68.88,
"imUrl": "http://ecx.images-amazon.com/images/I/51fAmVkTbyT._SY300_.jpg",
"related":
{
"also_bought": ["B00JHONN1S", "B002BZX8Z6", "B00D2K1M3O", "0000031909", "B00613WDTQ", "B00D0WDS9A", "B00D0GCI8S", "0000031895", "B003AVKOP2", "B003AVEU6G"],
"also_viewed": ["B002BZX8Z6", "B00JHONN1S", "B008F0SU0Y", "B00D23MC6W", "B00AFDOPDA", "B00E1YRI4C", "B002GZGI4E", "B003AVKOP2", "B00D9C1WBM", "B00CEV8366"],
"bought_together": ["B002BZX8Z6"]
},
"salesRank": {"Clothing, Shoes & Jewelry": 1368},
"brand": "Onitsuka",
"categories": [["Clothing, Shoes & Jewelry", "Men", "Shoes", "Fashion Sneakers"], ["Sports & Outdoors", "Exercise & Fitness", "Running", "Footwear"]]
}
{ "asin": "B0147ZZKQ2", "title": "Onitsuka Tiger Ultimate 81 Running Shoe", "price": 68.88, "imUrl": "http://ecx.images-amazon.com/images/I/51fAmVkTbyT._SY300_.jpg", "related": { "also_bought": ["B00JHONN1S", "B002BZX8Z6", "B00D2K1M3O", "0000031909", "B00613WDTQ", "B00D0WDS9A", "B00D0GCI8S", "0000031895", "B003AVKOP2", "B003AVEU6G"], "also_viewed": ["B002BZX8Z6", "B00JHONN1S", "B008F0SU0Y", "B00D23MC6W", "B00AFDOPDA", "B00E1YRI4C", "B002GZGI4E", "B003AVKOP2", "B00D9C1WBM", "B00CEV8366"], "bought_together": ["B002BZX8Z6"] }, "salesRank": {"Clothing, Shoes & Jewelry": 1368}, "brand": "Onitsuka", "categories": [["Clothing, Shoes & Jewelry", "Men", "Shoes", "Fashion Sneakers"], ["Sports & Outdoors", "Exercise & Fitness", "Running", "Footwear"]] }
Converting the JSON to CSV format
Конвертация JSON в формат CSV
The json data comes zipped. Here’s how we can read it into a pandas dataframe (and save to csv, if necessary):
Данные json поступают в архиве. Вот как можно прочитать их в pandas dataframe (и при необходимости сохранить в csv):
import pandas as pd
import gzip
def parse(path):
g = gzip.open(path, 'rb')
for l in g:
yield eval(l)
def get_df(path):
i = 0
df = {}
for d in parse(path):
df[i] = d
i += 1
return pd.DataFrame.from_dict(df, orient='index')
df = get_df('metadata.json.gz')
df.to_csv('metadata_csv', index=False)
import pandas as pd import gzip def parse(path): g = gzip.open(path, 'rb') for l in g: yield eval(l) def get_df(path): i = 0 df = {} for d in parse(path): df[i] = d i += 1 return pd.DataFrame.from_dict(df, orient='index') df = get_df('metadata.json.gz') df.to_csv('metadata_csv', index=False)
The above approach reads the entire dataset and loads it into a dataframe. This is viable given the small data size (3.1gb). However, for larger datasets ( >100gb), we’ll need an alternative approach. One way is to read the zipped json and write to csv, row by row:
Подход выше читает весь набор данных и загружает его в dataframe. Это допустимо при небольшом размере данных (3,1 ГБ). Однако для более крупных наборов ( >100 ГБ) понадобится альтернативный подход. Один из вариантов — читать сжатый json и записывать в csv построчно:
import json
import gzip
import csv
def parse(path):
g = gzip.open(path, 'rb')
for l in g:
yield eval(l)
def json_to_csv(read_path, write_path):
csv_writer = csv.writer(open(write_path, 'w'))
i = 0
for d in parse(read_path):
if i == 0:
header = d.keys()
csv_writer.writerow(header)
i += 1
csv_writer.writerow(d.values())
json_to_csv('metadata.json.gz', 'metadata.csv')
import json import gzip import csv def parse(path): g = gzip.open(path, 'rb') for l in g: yield eval(l) def json_to_csv(read_path, write_path): csv_writer = csv.writer(open(write_path, 'w')) i = 0 for d in parse(read_path): if i == 0: header = d.keys() csv_writer.writerow(header) i += 1 csv_writer.writerow(d.values()) json_to_csv('metadata.json.gz', 'metadata.csv')
Formatting and cleaning the category data
Форматирование и очистка данных о категориях
Now that we’ve parsed the product data into a dataframe (and saved it to csv), we can begin working with the category data. Here’s how the category data for the Onitsuka Tiger shoes looks like:
Теперь, когда мы разобрали данные о товарах в dataframe (и сохранили их в csv), можно приступить к работе с данными о категориях. Вот как выглядят данные о категориях для кроссовок Onitsuka Tiger:
[["Clothing, Shoes & Jewelry", "Men", "Shoes", "Fashion Sneakers"],
["Sports & Outdoors", "Exercise & Fitness", "Running", "Footwear”]]
[["Clothing, Shoes & Jewelry", "Men", "Shoes", "Fashion Sneakers"], ["Sports & Outdoors", "Exercise & Fitness", "Running", "Footwear”]]
The shoes are listed under two categories: “Clothing, Shoes, and Jewelry” (CSJ) and “Sports & Outdoors” (S&O). This means they are cross-listed across both categories, ensuring they can be found if you browse either the CSJ or the S&O catalogue. (Yes, they’re so badass they qualify as both fashion sneakers and running footwear; check out the top review here: Whappoww!! Ninja sneaks for bosses of chill)
Кроссовки указаны в двух категориях: «Clothing, Shoes, and Jewelry» (CSJ) и «Sports & Outdoors» (S&O). Это значит, что они перекрёстно перечислены в обеих категориях, и их можно найти, просматривая как каталог CSJ, так и каталог S&O. (Да, они настолько крутые, что подходят и под модные сникеры, и под беговую обувь; посмотрите топовый отзыв здесь: Whappoww!! Ninja sneaks for bosses of chill)
The API will classify a product into its primary category (i.e., the first category provided), ensuring a one-to-one relationship between products and their respective categories. Often, guidelines exist for which category should be the primary one (e.g., Shoes should have CSJ categories as their primary category)
API классифицирует товар в его основную категорию (то есть первую указанную категорию), обеспечивая взаимно однозначное соответствие между товарами и их категориями. Часто существуют правила, какая категория должна быть основной (например, у обуви основной категорией должна быть CSJ).
Converting category data into category path strings
Преобразование данных о категориях в строки-пути категорий
First, we’ll parse the category from a list of lists to a string. Simultaneously, we’ll keep only the primary categories (i.e., first category in the list). After parsing, this is the resulting category:
Сначала мы преобразуем категорию из списка списков в строку. Одновременно мы оставим только основные категории (то есть первую категорию в списке). После разбора получается такая категория:
"Clothing, Shoes & Jewelry -> Men -> Shoes -> Fashion Sneakers"
"Clothing, Shoes & Jewelry -> Men -> Shoes -> Fashion Sneakers"
Note: My preference is to convert the category data into a category path, a single string connected by arrows (->). However, any format should work fine.
Примечание: я предпочитаю преобразовывать данные о категориях в путь категории — единую строку, соединённую стрелками (->). Однако подойдёт любой формат.
Here’s the code to how it’s done:
Вот код, как это делается:
def get_category_path(category_path_list):
"""
(Str of list of list(s)) -> str
Returns the category path given a string of list of lists of
categories. If there are more than one list of categories provided,
returns the category path from the first list.
>>> get_category_path("[['A', 'B', 'C'], ['D', 'E', 'F', 'G']]")
'A -> B -> C'
>>> get_category_path("[['P1', 'P2', 'P3', 'P4']]")
'P1 -> P2 -> P3 -> P4'
:type category_path_list: str
:param category_path_list: A string containing a list of at least
one list of categories
:return: A string showing the full category path of the FIRST
category in the list (assumed to be primary category)
"""
try:
return ' -> '.join(eval(category_path_list)[0])
except IndexError: # Error if the outer list is empty
return 'no_category'
except TypeError: # Error if the outer list is missing
return 'no_category'
# Create column for category path
df['category_path'] = df['categories'].apply(get_category_path)
def get_category_path(category_path_list): """ (Str of list of list(s)) -> str Returns the category path given a string of list of lists of categories. If there are more than one list of categories provided, returns the category path from the first list. >>> get_category_path("[['A', 'B', 'C'], ['D', 'E', 'F', 'G']]") 'A -> B -> C' >>> get_category_path("[['P1', 'P2', 'P3', 'P4']]") 'P1 -> P2 -> P3 -> P4' :type category_path_list: str :param category_path_list: A string containing a list of at least one list of categories :return: A string showing the full category path of the FIRST category in the list (assumed to be primary category) """ try: return ' -> '.join(eval(category_path_list)[0]) except IndexError: # Error if the outer list is empty return 'no_category' except TypeError: # Error if the outer list is missing return 'no_category' # Create column for category path df['category_path'] = df['categories'].apply(get_category_path)
Exclude data where title or category is missing
Исключение данных, где отсутствует название или категория
If either the title or category is missing, we won’t be able to use the product data to train our model. Thus, we’ll exclude products with incomplete data, as such:
Если отсутствует либо название, либо категория, мы не сможем использовать данные о товаре для обучения нашей модели. Поэтому мы исключим товары с неполными данными, вот так:
df.dropna(subset=['title'], inplace=True)
df = df[df['category_path'] != 'no_category']
df.dropna(subset=['title'], inplace=True) df = df[df['category_path'] != 'no_category']
We started with 9.43 million products. More than 1 million have either missing title or category, leaving us with 7.98 million products.
Мы начали с 9,43 млн товаров. Более чем у 1 млн отсутствует либо название, либо категория, и остаётся 7,98 млн товаров.
Exclude certain categories
Исключение определённых категорий
There are some categories of products where the title does not provide any information about the product category (e.g., Books, Movies, etc). These products are usually classified via alternative approaches. For example, the availability of ISBN data indicates the product is a book, while the availability of ratings (e.g., PG-13) indicates the product is a movie.
Есть категории товаров, где название не даёт никакой информации о категории товара (например, книги, фильмы и т. д.). Такие товары обычно классифицируются альтернативными способами. Например, наличие данных ISBN указывает, что товар — книга, а наличие рейтинга (например, PG-13) указывает, что товар — фильм.
We’ll exclude certain categories of products as follows, leaving us with 5.59 million products:
Мы исключим определённые категории товаров следующим образом, и у нас остаётся 5,59 млн товаров:
df = df[df['category_path_lvl1'] != 'Books']
df = df[df['category_path_lvl1'] != 'CDs & Vinyl']
df = df[df['category_path_lvl1'] != 'Movies & TV']
df = df[df['category_path_lvl1'] != 'Books'] df = df[df['category_path_lvl1'] != 'CDs & Vinyl'] df = df[df['category_path_lvl1'] != 'Movies & TV']
Note: You may have noticed that the code above filters on the column category_path_lvl1. This column contains the top level categories for products (e.g., “Electronics”, “Clothing, Shoes, and Jewelry”, Sports & Outdoors”). I’ll leave deriving this as an exercise for the audience ;)
Примечание: вы могли заметить, что код выше фильтрует по столбцу category_path_lvl1. Этот столбец содержит категории верхнего уровня для товаров (например, «Electronics», «Clothing, Shoes, and Jewelry», «Sports & Outdoors»). Вывод этого столбца я оставлю в качестве упражнения для аудитории ;)
Exclude non-deepest/non-narrowest categories
Исключение категорий, не являющихся самыми глубокими/узкими
In the list of categories, we’ll find some that seem to stop halfway, such as the first category path below:
В списке категорий мы найдём такие, которые как будто обрываются на полпути, как первый путь категории ниже:
"Clothing, Shoes & Jewelry -> Men -> Shoes"
"Clothing, Shoes & Jewelry -> Men -> Shoes -> Fashion Sneakers"
"Clothing, Shoes & Jewelry -> Men -> Shoes" "Clothing, Shoes & Jewelry -> Men -> Shoes -> Fashion Sneakers"
In this case, the latter category is deeper (and narrower) than the former. Classifying products to the deepest category helps shoppers find relevant products easier (given that the category is narrower). Thus, we’ll exclude products that are not at the deepest category.
В этом случае вторая категория глубже (и уже), чем первая. Классификация товаров в самую глубокую категорию помогает покупателям легче находить нужные товары (поскольку категория уже). Поэтому мы исключим товары, которые не находятся в самой глубокой категории.
Here’s one way to do it. We’ll first sort the categories and compare each category path with the next. If the category path is not in the next (i.e., the category path is not a substring of the next category), then it is a deepest category and we append it to a list. We’ll then keep only products with categories in our list.
Вот один из способов это сделать. Сначала мы отсортируем категории и сравним каждый путь категории со следующим. Если путь категории не содержится в следующем (то есть путь категории не является подстрокой следующей категории), то это самая глубокая категория, и мы добавляем её в список. Затем мы оставим только товары с категориями из нашего списка.
# Create df of category path counts
category_path_df = df.groupby('category_path').agg({'title': 'count'}).sort_values(by='title', ascending=False).reset_index()
category_path_df.sort_values(by='category_path', inplace=True)
category_path_df['category_path_next'] = category_path_df['category_path'].shift(-1)
category_path_df.fillna('no_comparison', inplace=True)
# Create list of category_paths which are deepest category
category_path_list = []
for i, value in category_path_df.iterrows():
category_path = value['category_path']
category_path_next = value['category_path_next']
if category_path not in category_path_next:
category_path_list.append(category_path)
# Create df of category_path
category_path_df = pd.DataFrame(category_path_list, columns=['category_path'])
# Keep only rows where the category is in category_df
df = df[df['category_path'].isin(category_path_df['category_path'])]
# Create df of category path counts category_path_df = df.groupby('category_path').agg({'title': 'count'}).sort_values(by='title', ascending=False).reset_index() category_path_df.sort_values(by='category_path', inplace=True) category_path_df['category_path_next'] = category_path_df['category_path'].shift(-1) category_path_df.fillna('no_comparison', inplace=True) # Create list of category_paths which are deepest category category_path_list = [] for i, value in category_path_df.iterrows(): category_path = value['category_path'] category_path_next = value['category_path_next'] if category_path not in category_path_next: category_path_list.append(category_path) # Create df of category_path category_path_df = pd.DataFrame(category_path_list, columns=['category_path']) # Keep only rows where the category is in category_df df = df[df['category_path'].isin(category_path_df['category_path'])]
Initially, we had 17.6 k categories. After excluding non-deepest categories, about 15 k categories (and 4.61 mil products) remain.
Изначально у нас было 17,6 тыс. категорий. После исключения категорий, не являющихся самыми глубокими, остаётся около 15 тыс. категорий (и 4,61 млн товаров).
Exclude categories that have too few products
Исключение категорий, в которых слишком мало товаров
Lastly, to ensure sufficient data to split into train and test sets, and train our model, we’ll exclude categories with less than 10 products. With a 50-50 train-test split, we’ll have at least five products to train per category. This shouldn’t be too difficult and I encourage you to try it out yourself.
Наконец, чтобы обеспечить достаточно данных для разбиения на обучающую и тестовую выборки и обучения нашей модели, мы исключим категории, в которых менее 10 товаров. При разбиении 50 на 50 на обучение и тест у нас будет как минимум пять товаров для обучения на каждую категорию. Это не должно быть слишком сложно, и я призываю вас попробовать сделать это самостоятельно.
After excluding products based on this condition, we’re left with 4.59 mil products.
После исключения товаров по этому условию у нас остаётся 4,59 млн товаров.
And we’re done!
И мы закончили!
Congratulations on making it this far!
Поздравляю, что добрались до этого места!
We’re done with the key steps to cleaning the category data. There may also be other cleaning involved, such as excluding temporary categories (i.e., “Black Friday Sales”, “11-11 Sales”, etc) but we shall not cover them.
Мы завершили ключевые шаги по очистке данных о категориях. Может потребоваться и другая очистка, например исключение временных категорий (то есть «Black Friday Sales», «11-11 Sales» и т. д.), но мы их рассматривать не будем.
As shown, there’s a lot of work to be done in acquiring, formatting, and cleaning of the data before we get to building a model to classify products. In this case, I’m thankful to Julian McAudley and his kind sharing of the Amazon product metadata.
Как показано, нужно проделать большую работу по получению, форматированию и очистке данных, прежде чем мы перейдём к построению модели классификации товаров. В этом случае я благодарен Julian McAuley и его великодушному предоставлению метаданных о товарах Amazon.
In the next article, we go into—yeap, you guessed it—more data cleaning and preparation (specific to titles). This basic data preparation is key to training our classifier with high accuracy.
В следующей статье мы перейдём — да, вы угадали — к ещё большей очистке и подготовке данных (специфичной для названий). Эта базовая подготовка данных — ключ к обучению нашего классификатора с высокой точностью.
p.s., I would greatly appreciate any feedback on process, code, writing style, etc in the comments below. Thank you!
p.s. Я был бы очень признателен за любую обратную связь по процессу, коду, стилю изложения и т. д. в комментариях ниже. Спасибо!
If you found this useful, please cite this write-up as:
Если этот материал оказался полезным, пожалуйста, цитируйте его так:
Yan, Ziyou. (Oct 2016). Product Classification API Part 1: Data Acquisition. eugeneyan.com. https://eugeneyan.com/writing/product-categorization-api-part-1-data-acquisition-and-formatting/.
Yan, Ziyou. (Oct 2016). Product Classification API Part 1: Data Acquisition. eugeneyan.com. https://eugeneyan.com/writing/product-categorization-api-part-1-data-acquisition-and-formatting/.
or
или
@article{yan2016acquisition,
title = {Product Classification API Part 1: Data Acquisition},
author = {Yan, Ziyou},
journal = {eugeneyan.com},
year = {2016},
month = {Oct},
url = {https://eugeneyan.com/writing/product-categorization-api-part-1-data-acquisition-and-formatting/}
}
@article{yan2016acquisition, title = {Product Classification API Part 1: Data Acquisition}, author = {Yan, Ziyou}, journal = {eugeneyan.com}, year = {2016}, month = {Oct}, url = {https://eugeneyan.com/writing/product-categorization-api-part-1-data-acquisition-and-formatting/} }
Join 11,800+ readers getting updates on machine learning, RecSys, LLMs, and engineering.
Присоединяйтесь к 11 800+ читателей, получающих обновления о machine learning, RecSys, LLM и инженерии.