Автор оригинала: Florents Tselai.
Это было первоначально опубликовано на моем сайте – tselai.com
Одним из моих самых приятных греховных удовольствий всегда было соскабливание паутины . Особенно в течение прошлого года я очистил бесчисленное количество веб-сайтов, как для удовольствия, так и для получения прибыли. От нишевых и основных интернет-магазинов до новостных агентств и литературных блогов удивительно, сколько интересных и чистых данных можно получить с помощью простых инструментов, таких как BeautifulSoup
– Я даже не буду упоминать, какой радостью был безголовый режим Chrome.
В этом посте я поиграю с данными, которые я почерпнул из Греческого винного интернет-магазина . Вместо извинений за то, что отправил еще несколько запросов на их сервер, я призываю всех просмотреть их каталог и, возможно, даже купить несколько бутылок.
Сам скребок довольно прост, и его можно найти на моей странице GitHub . Здесь я сосредоточусь на быстром исследовательском анализе полученных данных (1125 уникальных меток) с помощью стандартных пакетов python.
Сам скребок предоставляет довольно простой API. Вы запрашиваете данные для данной страницы wine, и она возвращает хороший dict
с данными, как это:
from houseofwine_gr import get get('http://www.houseofwine.gr/how/megas-oinos-skoura.html')
{'ageable': True, 'alcohol_%': 13.5, 'avg_rating_%': 82, 'color': 'Ερυθρός', 'description': 'Στιβαρό ερυθρό κρασί παλαίωσης. Βασίζεται σε χαρμάνι που παντρεύει το πικάντικο σκέρτσο του Αγιωργίτικου με την αυστηρή δύναμη του Cabernet Sauvignon, στη ζεστή και βανιλάτη αγκαλιά του δρύινου βαρελιού που τα φιλοξένησε κατά την 20μηνη παλαίωσή του. Ποιοτική ετικέτα για μεγάλα φαγοπότια ή εκλεκτά κελάρια.', 'drink_now': False, 'keep_2_3_years': False, 'n_votes': 21, 'name': 'Μέγας Οίνος Σκούρα 2014', 'price': 20.9, 'tags': ['Ήπιος', 'Ξηρός', 'Cabernet Sauvignon', 'Αγιωργίτικο'], 'url': 'http://www.houseofwine.gr/how/megas-oinos-skoura.html', 'year': 2014}
Мы начнем с определения некоторых matplotlib
эстетики.
%matplotlib inline import matplotlib.pyplot as plt import matplotlib as mpl import seaborn as sns plt.style.use('fivethirtyeight') mpl.rcParams['figure.figsize'] = (8,6) mpl.rcParams['font.size'] = 14 mpl.rcParams['font.family'] = 'Serif' mpl.rcParams['figure.facecolor'] = 'white' plt.rcParams['axes.facecolor']='white' plt.rcParams['axes.grid'] =False plt.rcParams['figure.facecolor']='white'
Мы загружаем дамп данных, созданных модулем houseofwine_gr.dump
. Вы также можете найти набор данных в .json, .csv и .xlsx
на странице GitHub.
import pandas as pd df = pd.read_json('./data/houseofwine.gr-wines.json', encoding='utf-8')
Мы заменяем пустые строки на np.nan
, чтобы облегчить их обработку для Панд
from numpy import nan df = df.replace('', nan, regex=True)
Мы переименовываем некоторые имена столбцов, содержащие специальные символы, чтобы использовать их в качестве собственных Фреймов данных
средств доступа.
df = df.rename(columns={'alcohol_%': 'alcohol', 'avg_rating_%': 'avg_rating'}, inplace=False)
Мы также присваиваем столбцам соответствующие типы.
df['alcohol'] = df.alcohol.astype(float) df['n_votes'] = df.n_votes.astype(int, errors='ignore') df['price'] = df.price.astype(float) df['year'] = df.year.astype(int, errors='ignore')
Давайте переведем значения столбцов color
с греческого на английский.
df['color'] = df.color.replace({'Λευκός': 'White', 'Ερυθρός': 'Red', 'Ροζέ': 'Rosé' } )
Вот цветовая гистограмма для нашего набора данных.
ax = df['color'].value_counts().plot('bar') ax.set(ylabel='Wines', title='Wine Color Frequency');
Давайте проверим распределение некоторых простых показателей для каждого вина.
fig, ((ax1, ax2), (ax3, ax4) ) = plt.subplots(ncols=2, nrows=2, figsize=(12,8)) df.year.dropna().astype(int).value_counts().plot('bar', ax=ax1) ax1.set(title='Production Year Frequency', xlabel='Year'); sns.distplot(df[df.alcohol < 100].alcohol.dropna(), ax=ax2) ax2.set(xlabel='Alcohol %', title='Alcohol % Distribution'); sns.distplot(df[df.price < 100].price.dropna(), ax=ax3) ax3.set(xlabel='Price (< 100)') sns.distplot(df.avg_rating.dropna(), ax=ax4) ax4.set(xlabel='Average Rating');
Непосредственным наблюдением является почти нормальное распределение, которое можно увидеть для столбца Средняя оценка
с высоким средним значением 85+. Кроутонер на Reddit объясняет, почему это происходит (а также исправляет мою предыдущую ошибку):
Типичная шкала оценки вина составляет 50-100, а не 0-100. Таким образом, то, что выглядело как дистрибутив с поддержкой только половины, на самом деле является почти полностью поддерживаемым дистрибутивом с небольшим левым перекосом. Кроме того, существует огромная разница в культурном отношении между винами с рейтингом ниже и выше 90, причем вина, получившие рейтинг выше 90, обычно считаются значительно лучше, а также продаются значительно лучше. Этот культурный факт полностью меняет разумную интерпретацию данных. Большинство вин оцениваются как хорошие, и только небольшая часть оценивается как действительно хорошие.
Чтобы продвинуться дальше, давайте взглянем на столбец теги
.
0 [Riesling, Ήπιος, Ημίγλυκος] 1 [Ήπιος, Sauvignon Blanc, Ξηρός] 2 [Ήπιος, Ξηρός, Ξινόμαυρο, Merlot] 3 [Ήπιος, Ξηρός, Ασύρτικο, Μαλαγουζιά] 4 [Ήπιος, Ξηρός, Pinotage] 5 [Ήπιος, Sangiovese, Ξηρός] 6 [Ήπιος, Ξηρός, Ξινόμαυρο] 7 [Cabernet Sauvignon, Ήπιος, 13, Ξηρός, Merlot,... 8 [Ήπιος, Γλυκός] 9 [Ήπιος, Αηδάνι, Ξηρός] Name: tags, dtype: object
Похоже, что каждый список тегов может дать нам информацию о различных атрибутах вина (сортах, сладости и т. Д.), Поэтому нам было бы лучше, если бы мы сделали эти атрибуты отдельными столбцами.
Сначала мы преобразуем теги
элементы столбцов из списка
в набор
, так как это облегчит манипуляции. То есть вместо того, чтобы запутываться в if x in-else-try-except-IndexError
мы будем использовать set
операции. Скажите, что вы хотите о пределе в 79 символов и удобочитаемости; в интерактивном режиме однострочные и неизменяемые операции.
df['tags'] = df.tags.map(set)
Теперь мы выполняем несколько простых операций, чтобы извлечь информацию о сладости, мягкости и о том, является ли она игристой. Мы также переводим с греческого на английский.
sweetness_values = {'Γλυκός', 'Ημίγλυκος', 'Ξηρός', 'Ημίξηρος'} df['sweetness'] = df.tags.map(sweetness_values.intersection).map(lambda x: x.pop() if x else None) translations = {'Γλυκός': 'Sweet', 'Ημίγλυκος': 'Semi-Sweet', 'Ξηρός': 'Dry', 'Ημίξηρος': 'Semi-Dry'} df['sweetness'] = df['sweetness'].replace(translations) df['sparkling'] = df.tags.map({'Αφρώδης', 'Ημιαφρώδης'}.intersection ).map(lambda x: x.pop() if x else None ).replace({'Αφρώδης': 'Sparkling', 'Ημιαφρώδης': 'Semi-Sparkling'}) df['sparkling'] = df.sparkling.fillna('Not Sparkling') df['is_mild'] = df.tags.map(lambda x: 'Ήπιος' in x)
Вот гистограмма для каждого из этих 4 атрибутов.
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(nrows=2, ncols=2, sharex=True, squeeze=False) for attr, ax in zip(['sweetness', 'color', 'sparkling', 'is_mild'], (ax1, ax2, ax3, ax4)): df[attr].value_counts().sort_values(ascending=True).plot(kind='barh', ax=ax) attr_str = 'Mildness' if attr is 'is_mild' else attr.title() ax.set(xlabel='Number of Wines', title='{} Frequency'.format(attr_str)); fig.set_size_inches((12,8)) fig.tight_layout()
На этом этапе мы можем (почти) с уверенностью предположить, что все оставшиеся теги указывают сортовую информацию для каждого вина, и поэтому мы определяем новый столбец для их хранения.
non_varietal_tags = {'Αφρώδης', 'Ημιαφρώδης', 'Ξηρός', 'Ημίξηρος', 'Γλυκός', 'Ημίγλυκος', 'Ήπιος'} df['varieties'] = df.tags.map(lambda t: t.difference(non_varietal_tags))
Из-за ошибки синтаксического анализа в столбце появляются некоторые целые числа в виде разновидностей, поэтому мы их отфильтровываем.
def is_not_int(x): try: int(x) return False except ValueError: return True df['varieties'] = df.varieties.map(lambda x: set(filter(is_not_int, x)))
Мы также можем добавить логическую переменную того, является ли вино сортовым . Мы определяем как сортовое вино то, которое имеет в своем составе только один сорт. Остальные – по крайней мере с двумя разновидностями в их смеси – являются смесями .
df['is_varietal'] = df.varieties.map(set. __len__ ) == 1
Для сортовых вин, в частности, мы устанавливаем single_variety
– это значение будет NaN
для остальных не сортовых вин.
df.loc[df.is_varietal, 'single_variety'] = df.loc[df.is_varietal, 'varieties'].map(lambda v: next(iter(v)))
df['is_blend'] = df.varieties.map(set. __len__ ) >= 2
df.loc[df.is_varietal, 'variety_type'] = 'Varietal' df.loc[df.is_blend, 'variety_type'] = 'Blend'
Давайте посмотрим, как выглядит распределение сортов/смесей.
ax = df.is_varietal.replace({True: 'Varietal', False: 'Blend'} ).value_counts().plot('barh') ax.set(title='How many wines are varietals and how many blends ?');
Давайте покопаемся в сортах – должно быть интересно.
Вот несколько показательных сюжетов.
fig, (ax1, ax2, ax3) = plt.subplots(nrows=3, figsize=(12,14)) fig.tight_layout varieties_hist = df[df.is_varietal].varieties.map(lambda x: next(iter(x))).value_counts() varieties_hist.head(10).sort_values(ascending=True).plot('barh', ax=ax1) ax1.set(title='Most Frequent Varietal Wines (Top 10)'); varietals_mean_price = df[['single_variety', 'price']].groupby('single_variety').mean().dropna()['price'] varietals_mean_price.sort_values(ascending=False).head(10).sort_values(ascending=True).plot('barh', ax=ax2) ax2.set(title='Most Expensive Varietals', xlabel='', ylabel=''); varietals_mean_rating = df[['single_variety', 'avg_rating']].groupby('single_variety').mean().dropna()['avg_rating'] varietals_mean_rating.sort_values(ascending=False).head(10).sort_values(ascending=True).plot('barh', ax=ax3); ax3.set(title='Top Rated Varietals', ylabel=''); fig.tight_layout()
Похоже, Шардоне
является самым популярным сортом, в то время как Видаль
и Санджовезе
являются самыми дорогими. Самый рейтинговый – Мальвазия
, но все разновидности довольно близки.
Переключив наше внимание на смеси, мы делаем некоторые Numpy
и Scikit-Learn
magic, чтобы создать матрицу совпадений
разновидностей, появляющихся в смесях.
def create_coocurrence_df(docs): # Source: https://stackoverflow.com/a/37822989 count_model = CountVectorizer(lowercase=False, min_df=.1) # default unigram model X = count_model.fit_transform(docs) Xc = (X.T * X) # this is co-occurrence matrix in sparse csr format Xc.setdiag(0) # sometimes you want to fill same word cooccurence to 0 ret = pd.DataFrame(Xc.todense())#, index=count_model.get_feature_names(), columnscou=count_model.get_feature_names()) ret.index = ret.columns = list(map(lambda f: f.replace('_', ' '), count_model.get_feature_names())) return ret
from sklearn.feature_extraction.text import CountVectorizer docs = df.loc[df.is_blend, 'varieties'].map(lambda x: {s.replace(' ', '_') for s in x}).map(lambda x: ' '.join(x)) coocurrence = create_coocurrence_df(docs)
Это те сорта, которые чаще всего появляются в смесях.
ax = coocurrence.sum().sort_values(ascending=False).head(10).sort_values(ascending=True).plot(kind='barh') ax.set(title='Most Frequent Varieties Appearing in Blends (Top 10)');
А вот тепловая карта, показывающая, какие сорта обычно смешиваются вместе.
ax = sns.heatmap(coocurrence, square=True, annot=True, fmt="d", cmap='Blues') ax.set(title='Which varieties are usually blended together ?\n'.title()); plt.gcf().set_size_inches((8,6))
fig, axes = plt.subplots(nrows=3, figsize=(12, 18)) for c, ax in zip(['Red', 'White', 'Rosé'], axes): docs = df.loc[df.is_blend & (df.color==c), 'varieties'].map(lambda x: {s.replace(' ', '_') for s in x}).map(lambda x: ' '.join(x)) cooc = create_coocurrence_df(docs) cmaps = {'Red': 'Reds', 'White': 'Blues', 'Rosé': sns.cubehelix_palette(8)} sns.heatmap(cooc, square=True, annot=True, fmt="d", cmap=cmaps.get(c), ax=ax) ax.set(title='Usually blended varieties for {}\n'.format(c).title()); plt.tight_layout()
Вот и все, ребята. Потенциальным следующим шагом будет классификатор, основанный на текстовом описании
столбце, который я здесь не касался. Не стесняйтесь комментировать идеи или играть с данными самостоятельно.
Это было первоначально опубликовано на моем сайте – tselai.com