Определение популярности музыкального жанра по местоположению и создание интерактивной карты
В прошлом году я прочитал эту статью о Бренте Файязе, певце, который отказался от аванса на четверть миллиона долларов, чтобы остаться независимым. Ему не нужен был лейбл, менеджер Файяза мог общаться с поклонниками и планировать концерты, используя данные слушателей Spotify.
Как музыкант-любитель и специалист по обработке данных, я был очень взволнован перспективой артистов, использующих данные, чтобы оставаться самостоятельными. Эта история вдохновила меня на создание чего-то бесплатного, что помогло бы музыкантам продвигать себя.
В этой статье рассказывается, как я:
- Сбор данных о популярности музыкального жанра по местоположению с помощью библиотек Python Beautiful Soup и urllib.
- Создал интерактивную карту городов-рынков с помощью Plotly.
Эта статья предназначена для людей, имеющих некоторый опыт программирования на Python. Он будет охватывать конкретные приложения веб-скрейпинга и построения графиков с помощью Plotly, но не слишком подробный.
Чтобы узнать больше о веб-парсинге, прочтите эту статью или ознакомьтесь с моим кодом на GitHub.
Первой проблемой было определение полезного и бесплатного источника данных. Я просмотрел десятки веб-сайтов, статей, отчетов и рейтингов, пока не наткнулся на Everynoise.com.
На этом сайте, созданном Гленном Макдональдом из Spotify, собраны данные обо всех мыслимых музыкальных жанрах. На одной странице сайта перечислены лучшие музыкальные жанры в каждом городе, где используется Spotify.
Я смог подтвердить, что на странице перечислены города, в которых было наибольшее количество слушателей Spotify, сравнив его с другими статьями.
Данные Every Noise потенциально могли показать лучшие города, в которых можно продавать определенные музыкальные жанры, поэтому я решил собрать их и нанести на карту.
В этом проекте я использовал модули Python urllib и Beautiful Soup, которые упростили получение исходного кода HTML для любой страницы, используя всего несколько строк кода.
from bs4 import BeautifulSoup import urllib.request as urllib def fresh_soup(url): # when making requests identify as if using the Mozilla Browser hdr = {‘User-Agent’: ‘Mozilla/5.0’} # make a url request using the specified browser req = urllib.Request(url,headers=hdr) # retrieve the page source source = urllib.urlopen(req,timeout=10).read() soup = BeautifulSoup(source,”lxml”) return soup
Чтобы собрать остальную часть парсера, я начал перемещаться по веб-сайту и планировать, как он организован.
Я быстро заметил, что когда я нажимаю на название города, он показывает рейтинг самых популярных жанров и меняет URL, добавляя поисковый запрос в конце.
Это означало, что я мог получить самые популярные жанры в каждом месте, составив список с названиями или ссылками на каждый город. Для того, чтобы составить этот список, нужно очистить страницу Every Place at Once.
Для этого я щелкнул правой кнопкой мыши по названию первого города на первой странице и нажал «осмотреть». Это открыло исходный код страницы в моем браузере. Я пролистал несколько строк кода и нашел ссылки для каждого города.
Каждая строка с тегом tr
и классом datarow
содержала название и ссылку для города. Они были в разделе table
.
Используя источник страницы, я собрал список всех table
элементов, используя table = soup.find_all('table')
, и обнаружил, что вторая таблица была той, которая меня интересовала. Команда table.find_all('tr')
дала мне каждую строку в таблице.
В каждой строке информация сохранялась в первом элементе с тегом a
. К счастью, Beautiful Soup предоставляет легкий доступ к свойствам HTML. Я использовал a.text
для названий городов и a['href']
для необработанных ссылок.
def get_cities():
# url for a list of every city listed in everynoise
origin = 'http://everynoise.com/everyplace.cgi?root=all'
# get the formatted page source
soup = fresh_soup(origin)
# retrieve a list of all table
tags and grab the second table
table = soup.find_all('table')[1]
links = []
# loop through every row in the table
for row in table.find_all('tr'):
# extract the ['icon','city name','country code']
elements = [a.text for a in row.find_all('a')]
# there was only ever one tag which had the link in it
link = [a['href'] for a in row.find_all('a')][0]
# save each link in a (city,country,link) tuple
links.append((elements[1], elements[2], link))
return links
Я составил свой список названий городов и ссылок, но в некоторых городах использовались неанглийские символы, что приводило к ошибочным URL-адресам. Я создал функцию, которая использовала метод quote
из urllib.parse
для их исправления.
from urllib.parse import quote def clean_url(url): non_conformists = [s for s in url if s not in string.printable] for s in non_conformists: # and use the quote function to translate them url = url.replace(s,quote(s)) return url
Следующим шагом в создании парсера было извлечение жанров из каждой ссылки на город. Я повторил процесс щелчок правой кнопкой мыши → осмотреть, чтобы узнать, где найти лучшие жанры для каждого города, а затем написал функцию, которая циклически перемещалась по городам, переходила к каждой ссылке и сохраняла лучшие жанры в одном большом набор данных.
В процессе работы я заметил, что, несмотря на то, что они упорядочены от большинства к наименее слушаемым, у жанров, которые идут вверху, также есть более крупные шрифты, чем у тех, что внизу.
Это означало, что конкурентоспособность рынков можно было охарактеризовать на основе того, сколько жанров имеют общий размер шрифта. Я использовал пакет регулярных выражений, чтобы получить размер шрифта для каждого жанра.
import pandas as pd import re def genre_popularity(links, filename): # create a dataset where you will store everything everynoise_popularity = pd.DataFrame() # each link is a (city, country, link) tuple for link in links: soup = fresh_soup(link[2]) # find the table with the most popular genres genres = soup.find_all('div', {'class':'note'}) # if we find a table we proceed if len(genres)>0: # get the table genres = soup.find_all('div', {'class':'note'})[0] # and every row in the table excluding the header genres = genres.find_all('div')[1:] popularity = [] genre = [] for element in genres: # use the font-size as a proxy for popularity popularity.append(re.findall('font-size: ([0-9]+)%;', str(element))[0]) genre.append(element.text) df = pd.DataFrame({"Popularity":popularity,"Genre":genre}) df['City'] = link[0] df['Country Code'] = link[1] everynoise_popularity = everynoise_popularity.append(df,ignore_index=True,sort=False) everynoise_popularity.to_csv(filename) return everynoise_popularity
На этом я закончил скребок для Every Noise.
Затем мне нужно было добавить географическую информацию. Я получил долготу и широту для каждого города с simplemaps.com.
Названия городов не полностью совпадали с данными Every Noise, поэтому я использовал библиотеку fuzzymatcher для объединения двух наборов данных (я использовал эту библиотеку, потому что она работала непосредственно с фреймами данных; другие библиотеки, такие как FuzzyWuzzy, могут дать лучшие совпадения. ).
Каждая строка в окончательном наборе данных представляла собой пару жанр-город с рейтингом жанра и его популярностью в городе, географической информацией о городе и т. Д.
Следующей большой задачей было создать разумный способ измерить, насколько важны городские рынки для определенных жанров. Это потребовало выбора целевых жанров из более чем 1500 уникальных поджанров в данных Every Noise.
Чтобы лучше понять доступные жанры, я получил частоту каждого слова в каждом термине в столбце жанр и частоту их появления в уникальных жанрах (верхняя таблица , Freq) и в исходном рейтинге (PopFreq).
Я решил составить карту лучших рынков для музыки рэп, поэтому я остановился на словах рэп и хип-хоп, чтобы определить интересующие жанры и поджанры. и собрали их в список соответствующих поджанров (нижняя таблица).
Определив набор целевых жанров, я приступил к измерению рыночной значимости.
Метрика учитывала три вещи:
- Сколько релевантных поджанров появилось в одном рейтинге городов.
- Порядок поджанров в рейтинге городов.
- Порядок городов в списке доступных городов (относительный размер рынка Spotify).
Весь код для создания метрики был помещен в функцию, которая могла идентифицировать рынки отдельных жанров (рынки хип-хопа и рынки рэпа по отдельности) или коллективные рынки ( hiphop или rap вместе), в зависимости от отправленных ключевых слов.
Это позволит создавать подробные, но потенциально широкие рыночные диаграммы.
Последней задачей было составить карту музыкальных рынков.
Я использовал Mapbox
функцию в Plotly , которая была очень простой. Для создания фигуры Plotly требовалось всего два аргумента: data
и layout
.
Аргумент data
был списком графических объектов Plotly (тип графика, настройки графика и необработанные данные). Аргумент layout
допускает дополнительные корректировки и элементы рисунка.
Я создал Scattermapbox
объект-график для каждого набора данных (рэп и хип-хоп), установив их столбцы lat и long как широту и долготу Scattermapbox
.
Размер точки для каждого города был пропорционален рыночной значимости с помощью опции go.scattermapbox.Marker
.
Наконец, я добавил аргумент для opacity
, чтобы слои не блокировали друг друга.
import plotly.graph_objs as go from plotly.offline import plot def map_plot_element(ds, data_name, opacity_value): element = go.Scattermapbox( # the plot points for a Plotly map lat=ds['lat'], # define the latitude data lon=ds['lng'], # define the longitude data mode='markers', # define the point types opacity=opacity_value, marker=go.scattermapbox.Marker( # marker size based on market importance size=ds['market_importance']), # the dataset name for the legend name = data_name, # display the city name and top genres when hovering over text=ds['city'].str.title()+'<br><br>'+ds['genre'].str.title().str.replace(',','<br>'), ) return element data = [] data.append(map_plot_element(rap_data, "Rap", 1)) data.append(map_plot_element(hh_data, "Hip Hop", 0.9))
Аргументом макета был словарь, в котором можно было установить значения высоты, поля, размера шрифта и координат центра.
layout = dict( height = 500, margin = dict( t=0, b=0, l=0, r=0 ), # margins font = dict( color='#FFFFFF', size=11 ), # set font properties paper_bgcolor = '#000000', # set the paper's background color mapbox=dict( accesstoken=mapbox_access_token, bearing=0, center=dict( lat=0, # set the mapbox center lon=0 ), pitch=0, zoom=1.2, style='dark' # set the graph style ), )
Затем я передал объекты data
и layout
методу Figure
, используя fig = go.Figure(data=data, layout=layout)
, и построил фигуру, используя plot(fig)
, в результате чего появилась карта ниже.
При наведении курсора на каждый город отображалось название города и соответствующие поджанры, но я чувствовал, что некоторые дополнительные элементы помогут сделать карту более удобной для пользователя.
К счастью, я мог использовать словарь макета для добавления меню, обновляющего карту.
Я создал два меню обновления, первое было раскрывающимся, изменяющим стиль графика между dark
, light
, satellite
и satellite with streets
.
drop_down_1 = dict( # drop down menu dict buttons=list([ # buttons are listed manually dict( # button 1 # arguments for the first button args=['mapbox.style', 'dark'], # set style to dark label='Dark', # button text "Dark" method='relayout' # relayout update method ), dict( # button 2 args=['mapbox.style', 'light'], # set style to light label='Light', method='relayout' ), dict( # button 3 args=['mapbox.style', 'satellite'], # set style to satelite label='Satellite', method='relayout' ), dict( # button 4 # set style to satelite with streets args=['mapbox.style', 'satellite-streets'], label='Satellite with Streets', method='relayout' )]), direction = 'up', # the menu opens upwards x = 0.75, # distance from anchors xanchor = 'left', # anchor direction y = 0.05, yanchor = 'bottom', bordercolor = '#FFFFFF', font = dict(size=11) # font size )
Во втором меню обновления перечислены все страны в наборе данных и увеличен центроид выбранной страны.
Первым шагом было создание значения по умолчанию World
с настройками центра и масштаба карты по умолчанию.
Используя широту и долготу центроидов стран с Periscopedata.com, я создал кнопки увеличения для каждой страны и добавил их в список кнопок.
# create the first element of the scroll down list countries=list([ dict( args=[ { # set the default view to the cetner of the world 'mapbox.center.lat':0, 'mapbox.center.lon':0, 'mapbox.zoom':1.2, # with a high zoom 'annotations[0].text':'World View' # titled "World View" } ], label='World', # give it the layout method='relayout' ) ]) # loop through each country to add their data to the drop-down menu for idx, row in country_data.iterrows(): countries.append( dict( args=[{ # element centers on the latitude and longitude 'mapbox.center.lat': row['latitude'], 'mapbox.center.lon': row['longitude'], # zoom to only see the country 'mapbox.zoom':3, # display the country code, name and preferred sub-genre 'annotations[0].text':'<br>'.join([row['country'], row['name'],]) }], label=row['name'], method='relayout', ) ) drop_down_2 = dict( buttons = countries, # buttons are the countries pad = {'r': 0, 't': 10}, x = 0.03, # space from x anchor xanchor = 'left', # anchor it to the left y = 1.0, # space from y anchor yanchor = 'top', # anchor to the top bgcolor = '#AAAAAA', # menu background color active = 99, bordercolor = '#FFFFFF', font = dict(size=11, color='#000000') )
Включить раскрывающиеся меню на карту было так же просто, как поместить их в список и установить для клавиши updatemenu
в layout
словаре: layout['updatemenus'] = [drop_down_1, drop_down_2]
.
Наконец-то! У меня было географическое представление музыкальных рынков и возможность принимать более обоснованные решения о том, где и как потратить маркетинговый бюджет на мое первое музыкальное видео!
Я создал две записные книжки, которые проходят через процессы очистки и сопоставления, описанные в этой статье.
Код все еще может быть улучшен, особенно сопоставление широты и долготы для городов, которое все еще приводит к некоторым несоответствиям.
Вы можете взаимодействовать с графиками на Datapane.com.