Этап 1. Получение данных Изучим данные, предоставленные сервисом для проекта. Импорт библиотек # <импорт библиотеки pandas> import pandas as pd Прочитаем файл music_project.csv и сохраним его в переменной df. # <чтение файла с данными с сохранением в df> df = pd.read_csv('/datasets/music_project.csv') Получение первых 10 строк таблицы. # <получение первых 10 строк таблицы df> df.head(10) userID Track artist genre City time Day 0 FFB692EC Kamigata To Boots The Mass Missile rock Saint-Petersburg 20:28:33 Wednesday 1 55204538 Delayed Because of Accident Andreas Rönnberg rock Moscow 14:07:09 Friday 2 20EC38 Funiculì funiculà Mario Lanza pop Saint-Petersburg 20:58:07 Wednesday 3 A3DD03C9 Dragons in the Sunset Fire + Ice folk Saint-Petersburg 08:37:09 Monday 4 E2DC1FAE Soul People Space Echo dance Moscow 08:34:34 Monday 5 842029A1 Преданная IMPERVTOR rusrap Saint-Petersburg 13:09:41 Friday 6 4CB90AA5 True Roman Messer dance Moscow 13:00:07 Wednesday 7 F03E1C1F Feeling This Way Polina Griffith dance Moscow 20:47:49 Wednesday 8 8FA1D3BE И вновь продолжается бой NaN ruspop Moscow 09:17:40 Friday 9 E772D5C0 Pessimist NaN dance Saint-Petersburg 21:20:49 Wednesday Общая информация о данных таблицы df. # <получение общей информации о данных в таблице df.info() RangeIndex: 65079 entries, 0 to 65078 Data columns (total 7 columns): userID 65079 non-null object Track 63848 non-null object artist 57876 non-null object genre 63881 non-null object City 65079 non-null object time 65079 non-null object Day 65079 non-null object dtypes: object(7) memory usage: 3.5+ MB Рассмотрим полученную информацию подробнее. Всего в таблице 7 столбцов, тип данных у каждого столбца - < object >. Подробно разберём, какие в df столбцы и какую информацию они содержат: userID — идентификатор пользователя; Track — название трека; artist — имя исполнителя; genre — название жанра; City — город, в котором происходило прослушивание; time — время, в которое пользователь слушал трек; Day — день недели. Количество значений в столбцах различается. Это говорит о том, что в данных есть <пустые> значения. Выводы Каждая строка таблицы содержит информацию о композициях определённого жанра в определённом исполнении, которые пользователи слушали в одном из городов в определённое время и день недели. Две проблемы, которые нужно решать: пропуски и некачественные названия столбцов. Для проверки рабочих гипотез особенно ценны столбцы time, day и City. Данные из столбца genre позволят узнать самые популярные жанры. Этап 2. Предобработка данных Исключим пропуски, переименуем столбцы, а также проверим данные на наличие дубликатов. Получаем перечень названий столбцов. Какая наблюдается проблема — кроме тех, что уже были названы ранее? # <перечень названий столбцов таблицы df> df.columns Index([' userID', 'Track', 'artist', 'genre', ' City ', 'time', 'Day'], dtype='object') В названиях столбцов есть пробелы, которые могут затруднять доступ к данным. Переименуем столбцы для удобства дальнейшей работы. Проверим результат. # <переименование столбцов> #df.columns = ['userID', 'track_name', 'artist_name', 'genre_name', 'city', 'time', 'weekday'] df.set_axis(['user_id', 'track_name', 'artist_name', 'genre_name', 'city', 'time', 'weekday'], axis = 'columns', inplace = True) # <проверка результатов - перечень названий столбцов> list(df) ['user_id', 'track_name', 'artist_name', 'genre_name', 'city', 'time', 'weekday'] Проверим данные на наличие пропусков вызовом набора методов для суммирования пропущенных значений. # <суммарное количество пропусков, выявленных методом isnull() в таблице df> df.isnull().sum() #df.columns[df.isnull().any()] # <названия столбцов с наличием пропусков в таблице df> #df[df.columns[df.isnull().any()]]# <названия столбцов с наличием пропусков в таблице df с выводом данных таблицы> user_id 0 track_name 1231 artist_name 7203 genre_name 1198 city 0 time 0 weekday 0 dtype: int64 Пустые значения свидетельствуют, что для некоторых треков доступна не вся информация. Причины могут быть разные: скажем, не назван конкретный исполнитель народной песни. Хуже, если проблемы с записью данных. Каждый отдельный случай необходимо разобрать и выявить причину. Заменяем пропущенные значения в столбцах с названием трека и исполнителя на строку 'unknown'. После этой операции нужно убедиться, что таблица больше не содержит пропусков. # <замена пропущенных значений в столбце 'track_name' на строку 'unknown' специальным методом замены> df['track_name'] = df['track_name'].fillna('unknown') # <замена пропущенных значений в столбце 'artist_name' на строку 'unknown' специальным методом замены> df['artist_name'] = df['artist_name'].fillna('unknown') # <проверка: вычисление суммарного количества пропусков, выявленных в таблице df> df.isnull().sum() user_id 0 track_name 0 artist_name 0 genre_name 1198 city 0 time 0 weekday 0 dtype: int64 Удаляем в столбце с жанрами пустые значения; убеждаемся, что их больше не осталось. # <удаление пропущенных значений в столбце 'genre_name'> df.dropna(subset=['genre_name'],inplace=True) # <проверка> df.isnull().sum() user_id 0 track_name 0 artist_name 0 genre_name 0 city 0 time 0 weekday 0 dtype: int64 Необходимо установить наличие дубликатов. Если найдутся, удаляем, и проверяем, все ли удалились. # <получение суммарного количества дубликатов в таблице df> df.duplicated().sum() 3755 # <удаление всех дубликатов из таблицы df специальным методом> df=df.drop_duplicates().reset_index(drop=True) # <проверка на отсутствие> df.duplicated().sum() 0 Дубликаты могли появиться вследствие сбоя в записи данных. Стоит обратить внимание и разобраться с причинами появления такого «информационного мусора». Сохраняем список уникальных значений столбца с жанрами в переменной genres_list. Объявим функцию find_genre() для поиска неявных дубликатов в столбце с жанрами. Например, когда название одного и того же жанра написано разными словами. # <сохранение в переменной genres_list списка уникальных значений, выявленных специальным методом в столбце 'genre_name'> genres_list=df['genre_name'].unique() # <создание функции find_genre()> # функция принимает как параметр строку с названием искомого жанра def find_genre(genre): # в теле объявляется переменная-счётчик, ей присваивается значение 0, count=0 # затем цикл for проходит по списку уникальных значений for i in genres_list: # если очередной элемент списка равен параметру функции, if i == genre: #if len(i) == genre: # то значение счётчика увеличивается на 1 count += 1 # по окончании работы цикла функция возвращает значение счётчика return count Вызов функции find_genre() для поиска различных вариантов названия жанра хип-хоп в таблице. Правильное название — hiphop. Поищем другие варианты: hip hop hip-hop # <вызовом функции find_genre() проверяется наличие варианта 'hip'> find_genre('hip') 1 # <проверяется наличие варианта 'hop'> find_genre('hop') 0 # <проверяется наличие варианта 'hip-hop'> find_genre('hip-hop') 0 Объявим функцию find_hip_hop(), которая заменяет неправильное название этого жанра в столбце 'genre_name' на 'hiphop' и проверяет успешность выполнения замены. Так исправляем все варианты написания, которые выявила проверка. # <создание функции find_hip_hop()> # функция принимает как параметры таблицу df и неверное название def find_hip_hop(df,wrong): # к столбцу 'genre_name' применяется специальный метод, # который заменяет второй параметр на строку 'hiphop' df['genre_name'] = df['genre_name'].replace(wrong, 'hiphop') # результат работы равен подсчитанному методом count() числу значений столбца, # которые равны второму параметру total = df.loc[df.loc[:,'genre_name'] == 'wrong']['genre_name'].count() # функция возвращает результат return total 0 # <замена одного неверного варианта на hiphop вызовом функции find_hip_hop()> find_hip_hop(df,'hip') #df['genre_name'] = df['genre_name'].replace('hip', 'hiphop') #total = df.loc[df.loc[:,'genre_name'] == 'hip']['genre_name'].count() #print(total) 0 Получаем общую информацию о данных. Убеждаемся, что чистка выполнена успешно. # <получение общей информации о данных таблицы df> df.info() print(genres_list) RangeIndex: 60126 entries, 0 to 60125 Data columns (total 7 columns): user_id 60126 non-null object track_name 60126 non-null object artist_name 60126 non-null object genre_name 60126 non-null object city 60126 non-null object time 60126 non-null object weekday 60126 non-null object dtypes: object(7) memory usage: 3.2+ MB ['rock' 'pop' 'folk' 'dance' 'rusrap' 'ruspop' 'world' 'electronic' 'alternative' 'children' 'rnb' 'hip' 'jazz' 'postrock' 'latin' 'classical' 'metal' 'reggae' 'tatar' 'blues' 'instrumental' 'rusrock' 'dnb' 'türk' 'post' 'country' 'psychedelic' 'conjazz' 'indie' 'posthardcore' 'local' 'avantgarde' 'punk' 'videogame' 'techno' 'house' 'christmas' 'melodic' 'caucasian' 'reggaeton' 'soundtrack' 'singer' 'ska' 'shanson' 'ambient' 'film' 'western' 'rap' 'beats' "hard'n'heavy" 'progmetal' 'minimal' 'contemporary' 'new' 'soul' 'holiday' 'german' 'tropical' 'fairytail' 'spiritual' 'urban' 'gospel' 'nujazz' 'folkmetal' 'trance' 'miscellaneous' 'anime' 'hardcore' 'progressive' 'chanson' 'numetal' 'vocal' 'estrada' 'russian' 'classicmetal' 'dubstep' 'club' 'deep' 'southern' 'black' 'folkrock' 'fitness' 'french' 'disco' 'religious' 'hiphop' 'drum' 'extrememetal' 'türkçe' 'experimental' 'easy' 'metalcore' 'modern' 'argentinetango' 'old' 'breaks' 'eurofolk' 'stonerrock' 'industrial' 'funk' 'jpop' 'middle' 'variété' 'other' 'adult' 'christian' 'gothic' 'international' 'muslim' 'relax' 'schlager' 'caribbean' 'ukrrock' 'nu' 'breakbeat' 'comedy' 'chill' 'newage' 'specialty' 'uzbek' 'k-pop' 'balkan' 'chinese' 'meditative' 'dub' 'power' 'death' 'grime' 'arabesk' 'romance' 'flamenco' 'leftfield' 'european' 'tech' 'newwave' 'dancehall' 'mpb' 'piano' 'top' 'bigroom' 'opera' 'celtic' 'tradjazz' 'acoustic' 'epicmetal' 'historisch' 'downbeat' 'downtempo' 'africa' 'audiobook' 'jewish' 'sängerportrait' 'deutschrock' 'eastern' 'action' 'future' 'electropop' 'folklore' 'bollywood' 'marschmusik' 'rnr' 'karaoke' 'indian' 'rancheras' 'электроника' 'afrikaans' 'tango' 'rhythm' 'sound' 'deutschspr' 'trip' 'lovers' 'choral' 'dancepop' 'podcasts' 'retro' 'smooth' 'mexican' 'brazilian' 'ïîï' 'mood' 'surf' 'author' 'gangsta' 'triphop' 'inspirational' 'idm' 'ethnic' 'bluegrass' 'broadway' 'animated' 'americana' 'karadeniz' 'rockabilly' 'colombian' 'self' 'synthrock' 'sertanejo' 'japanese' 'canzone' 'swing' 'lounge' 'sport' 'korean' 'ragga' 'traditional' 'gitarre' 'frankreich' 'alternativepunk' 'emo' 'laiko' 'cantopop' 'glitch' 'documentary' 'rockalternative' 'thrash' 'hymn' 'oceania' 'rockother' 'popeurodance' 'dark' 'vi' 'grunge' 'hardstyle' 'samba' 'garage' 'soft' 'art' 'folktronica' 'entehno' 'mediterranean' 'chamber' 'cuban' 'taraftar' 'rockindie' 'gypsy' 'hardtechno' 'shoegazing' 'skarock' 'bossa' 'salsa' 'latino' 'worldbeat' 'malaysian' 'baile' 'ghazal' 'loungeelectronic' 'arabic' 'popelectronic' 'acid' 'kayokyoku' 'neoklassik' 'tribal' 'tanzorchester' 'native' 'independent' 'cantautori' 'handsup' 'poprussian' 'punjabi' 'synthpop' 'rave' 'französisch' 'quebecois' 'speech' 'soulful' 'teen' 'jam' 'ram' 'horror' 'scenic' 'orchestral' 'neue' 'roots' 'slow' 'jungle' 'indipop' 'axé' 'fado' 'showtunes' 'arena' 'irish' 'mandopop' 'forró' 'popdance' 'dirty' 'regional'] Вывод На этапе предобработки в данных обнаружились не только пропуски и проблемы с названиями столбцов, но и всяческие виды дубликатов. Их удаление позволит провести анализ точнее. Поскольку сведения о жанрах важно сохранить для анализа, не просто удаляем все пропущенные значения, но заполним пропущенные имена исполнителей и названия треков. Имена столбцов теперь корректны и удобны для дальнейшей работы. Действительно ли музыку в разных городах слушают по-разному? Была выдвинута гипотеза, что в Москве и Санкт-Петербурге пользователи слушают музыку по-разному. Проверяем это предположение по данным о трёх днях недели — понедельнике, среде и пятнице. Для каждого города устанавливаем количество прослушанных в эти дни композиций с известным жанром, и сравниваем результаты. Группируем данные по городу и вызовом метода count() подсчитываем композиции, для которых известен жанр. # <группировка данных таблицы df по столбцу 'city' и подсчёт количества значений столбца 'genre_name'> df.groupby('city')['genre_name'].count() В Москве прослушиваний больше, чем в Питере, но это не значит, что Москва более активна. У Яндекс.Музыки в целом больше пользователей в Москве, поэтому величины сопоставимы. Сгруппируем данные по дню недели и подсчитаем прослушанные в понедельник, среду и пятницу композиции, для которых известен жанр. # <группировка данных по столбцу 'weekday' и подсчёт количества значений столбца 'genre_name'> df.groupby('weekday')['genre_name'].count() Понедельник и пятница — время для музыки; по средам пользователи немного больше вовлечены в работу. Создаём функцию number_tracks(), которая принимает как параметры таблицу, день недели и название города, а возвращает количество прослушанных композиций, для которых известен жанр. Проверяем количество прослушанных композиций для каждого города и понедельника, затем среды и пятницы. # <создание функции number_tracks()> # объявляется функция с тремя параметрами: df, day, city def number_tracks(df, day, city): # в переменной track_list сохраняются те строки таблицы df, для которых # значение в столбце 'weekday' равно параметру day # и одновременно значение в столбце 'city' равно параметру city track_list=df[(df['weekday']==day) & (df['city']==city)] # в переменной track_list_count сохраняется число значений столбца 'genre_name', # рассчитанное методом count() для таблицы track_list track_list_count = track_list['genre_name'].count() # функция возвращает значение track_list_count return track_list_count # <список композиций для Москвы в понедельник> number_tracks(df, 'Monday', 'Moscow') # <список композиций для Санкт-Петербурга в понедельник> number_tracks(df, 'Monday', 'Saint-Petersburg') # <список композиций для Москвы в среду> number_tracks(df, 'Wednesday', 'Moscow') # <список композиций для Санкт-Петербурга в среду> number_tracks(df, 'Wednesday', 'Saint-Petersburg') # <список композиций для Москвы в пятницу> number_tracks(df, 'Friday', 'Moscow') # <список композиций для Санкт-Петербурга в пятницу> number_tracks(df, 'Friday', 'Saint-Petersburg') Сведём полученную информацию в одну таблицу, где ['city', 'monday', 'wednesday', 'friday'] названия столбцов. # <таблица с полученными данными> data = [['Moscow', 15347, 10865, 15680], ['Saint-Petersburg', 5519, 6913, 5802]] columns = ['city','monday','wednesday','friday'] table = pd.DataFrame(data = data, columns = columns) table Вывод Результаты показывают, что относительно среды музыку в Петербурге и Москве слушают «зеркально»: в Москве пики приходятся на понедельник и пятницу, а в среду время прослушивания снижается. Тогда как в Санкт-Петербурге среда — день самого большого интереса к музыке, а в понедельник и пятницу он меньше, причём почти одинаково меньше. Утро понедельника и вечер пятницы — разная музыка или одна и та же? Ищем ответ на вопрос, какие жанры преобладают в разных городах в понедельник утром и в пятницу вечером. Есть предположение, что в понедельник утром пользователи слушают больше бодрящей музыки (например, жанра поп), а вечером пятницы — больше танцевальных (например, электронику). Получим таблицы данных по Москве moscow_general и по Санкт-Петербургу spb_general. # получение таблицы moscow_general из тех строк таблицы df, # для которых значение в столбце 'city' равно 'Moscow' moscow_general=df[df['city'] == 'Moscow'] # <получение таблицы spb_general> spb_general=df[df['city']=='Saint-Petersburg'] Создаём функцию genre_weekday(), которая возвращает список жанров по запрошенному дню недели и времени суток с такого-то часа по такой-то. # объявление функции genre_weekday() с параметрами df, day, time1, time2 def genre_weekday(df, day, time1, time2): # в переменной genre_list сохраняются те строки df, для которых одновременно: # 1) значение в столбце 'weekday' равно параметру day, # 2) значение в столбце 'time' больше time1 и # 3) меньше time2. genre_list=df[(df['weekday']==day) & (df['time']>time1)& (df['time']<=time2)] # в переменной genre_list_sorted сохраняются в порядке убывания # первые 10 значений Series, полученной подсчётом числа значений 'genre_name' # сгруппированной по столбцу 'genre_name' таблицы genre_list genre_list_sorted = genre_list.groupby('genre_name')['genre_name'].count().sort_values(ascending = False).head(10) # функция возвращает значение genre_list_sorted return genre_list_sorted Cравниваем полученные результаты по таблице для Москвы и Санкт-Петербурга в понедельник утром (с 7 до 11) и в пятницу вечером (с 17 до 23). # <вызов функции для утра понедельника в Москве (вместо df таблица moscow_general)> genre_weekday(moscow_general, 'Monday', '07:00:00', '11:00:00') # <вызов функции для утра понедельника в Петербурге (вместо df таблица spb_general)> genre_weekday(spb_general, 'Monday', '07:00:00', '11:00:00') # <вызов функции для вечера пятницы в Москве> genre_weekday(moscow_general, 'Friday', '17:00:00', '23:00:00') # <вызов функции для вечера пятницы в Питере> genre_weekday(spb_general, 'Friday', '17:00:00', '23:00:00') # <вызов функции для вечера пятницы в Питере> genre_weekday(spb_general, 'Friday', '17:00:00', '23:00:00') Популярные жанры в понедельник утром в Питере и Москве оказались похожи: везде, как и предполагалось, популярен поп. Несмотря на это, концовка топ-10 для двух городов различается: в Питере в топ-10 входит джаз и русский рэп, а в Москве жанр world. В конце недели ситуация не меняется. Поп-музыка всё так же на первом месте. Опять разница заметна только в концовке топ-10, где в Питере пятничным вечером тоже присутствует жанр world. Вывод Жанр поп безусловный лидер, а топ-5 в целом не различается в обеих столицах. При этом видно, что концовка списка более «живая»: для каждого города выделяются более характерные жанры, которые действительно меняют свои позиции в зависимости от дня недели и времени. Москва и Питер — две разные столицы, два разных направления в музыке. Правда? Гипотеза: Питер богат своей рэп-культурой, поэтому это направление там слушают чаще, а Москва — город контрастов, но основная масса пользователей слушает попсу. Сгруппируем таблицу moscow_general по жанру, сосчитаем численность композиций каждого жанра методом count(), отсортируем в порядке убывания и сохраним результат в таблице moscow_genres. Просмотрим первые 10 строк этой новой таблицы. # одной строкой: группировка таблицы moscow_general по столбцу 'genre_name', # подсчёт числа значений 'genre_name' в этой группировке методом count(), # сортировка Series в порядке убывания и сохранение в moscow_genres moscow_genres=moscow_general.groupby('genre_name')['genre_name'].count().sort_values(ascending = False) # <просмотр первых 10 строк moscow_genres> moscow_genres.head(10) Сгруппируем таблицу spb_general по жанру, сосчитаем численность композиций каждого жанра методом count(), отсортируем в порядке убывания и сохраним результат в таблице spb_genres. Просматриваем первые 10 строк этой таблицы. Теперь можно сравнивать два города. # <группировка таблицы spb_general, расчёт, сохранение в spb_genres> spb_genres=spb_general.groupby('genre_name')['genre_name'].count().sort_values(ascending=False) # <просмотр первых 10 строк spb_genres> spb_genres.head(10) Вывод В Москве, кроме абсолютно популярного жанра поп, есть направление русской популярной музыки. Значит, что интерес к этому жанру шире. А рэп, вопреки предположению, занимает в обоих городах близкие позиции. Этап 4. Результаты исследования Рабочие гипотезы: музыку в двух городах — Москве и Санкт-Петербурге — слушают в разном режиме; списки десяти самых популярных жанров утром в понедельник и вечером в пятницу имеют характерные отличия; население двух городов предпочитает разные музыкальные жанры. Общие результаты Москва и Петербург сходятся во вкусах: везде преобладает популярная музыка. При этом зависимости предпочтений от дня недели в каждом отдельном городе нет — люди постоянно слушают то, что им нравится. Но между городами в разрезе дней неделей наблюдается зеркальность относительно среды: Москва больше слушает в понедельник и пятницу, а Петербург наоборот - больше в среду, но меньше в понедельник и пятницу. В результате первая гипотеза <подтверждена>, вторая гипотеза <не подтверждена > и третья <не подтверждена >.