{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import pandas as pd\n", "# настроим значения отображения данных по умолчанию в графиках pandas\n", "plt.rc(\"figure\", figsize=(10, 6))\n", "np.set_printoptions(precision=4)\n", "pd.options.display.max_columns = 20\n", "pd.options.display.max_rows = 20\n", "pd.options.display.max_colwidth = 80" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Управление социального обеспечения США представило данные в виде набора файлов (по одному на каждый год), в которых указано общее число родившихся младенцев для каждой пары пол/имя. \n", "Архив этих файлов находится по адресу http://www.ssa.gov/oact/babynames/limits.html.\n", "\n", "Поскольку поля в исходном файле разделены запятыми, файл можно загрузить в объект DataFrame методом pandas.read_csv" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "names1880 = pd.read_csv(\"babynames/yob1880.txt\",\n", " names=[\"name\", \"sex\", \"births\"])\n", "names1880" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "С этим набором можно проделать много интересных вариантов аналитики:\n", "* наглядно представить долю младенцев, получавших данное имя (совпадающее с вашим или какое-нибудь другое) за весь период времени;\n", "* определить относительный ранг имени;\n", "* найти самые популярные в каждом году имена или имена, для которых фиксировалось наибольшее увеличение или уменьшение частоты;\n", "* проанализировать тенденции выбора имен: \n", " * количество гласных и согласных,\n", " * длину,\n", " * общее разнообразие, \n", " * изменение в  написании, \n", " * частотность первых/последних букв ...\n", "* проанализировать внешние источники тенденций: библейские имена, имена знаменитостей, демографические изменения.\n", "\n", "В эти файлы включены только имена, которыми были названы не менее пяти младенцев в году, поэтому для простоты сумму значений в столбце sex можно считать общим числом родившихся в данном году младенцев" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "names1880.groupby(\"sex\")[\"births\"].sum()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Поскольку в каждом файле находятся данные только за один год, то соберем все данные за ХХ век в единый объект DataFrame и добавим поле year. \n", "\n", "Это делается методом pandas.concat" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pieces = []\n", "for year in range(1900, 2001):\n", " path = f\"babynames/yob{year}.txt\"\n", " frame = pd.read_csv(path, names=[\"name\", \"sex\", \"births\"])\n", "\n", " # добавим колонку year\n", " frame[\"year\"] = year\n", " pieces.append(frame)\n", "\n", "# соединим все фреймы в один DataFrame\n", "names = pd.concat(pieces, ignore_index=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Обратите внимание на два момента. \n", "\n", "Во-первых, напомним, что concat по умолчанию объединяет объекты DataFrame построчно. \n", "\n", "Во-вторых, следует задать параметр ignore_index=True, потому что нам неинтересно сохранять исходные номера строк, прочитанных методом read_csv. \n", "\n", "В итоге получили очень большой DataFrame, содержащий данные обо всех именах." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "names" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Имея эти данные, можем приступить к агрегированию на уровне года и пола, используя метод _groupby_ или _pivot_table_" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "total_births = names.pivot_table(\"births\", index=\"year\", columns=\"sex\", aggfunc=sum)\n", "#total_birth_s = names.pivot_table(\"births\", index=\"year\", columns=\"sex\").sum()\n", "total_births.tail()\n", "total_births.plot(title=\"Всего рождений по году и полу\")\n", "pass" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Далее вставим столбец _prop_, содержащий долю младенцев (пропорцию), получивших данное имя, относительно общего числа родившихся. \n", "\n", "Значение _prop_, равное 0.02, означает, что данное имя получили 2 из 100 младенцев. Затем сгруппируем данные по году и полу и добавим в каждую группу новый столбец." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def add_prop(group):\n", " group[\"prop\"] = group[\"births\"] / group[\"births\"].sum()\n", " return group\n", "\n", "namesap = names.groupby([\"year\", \"sex\"], group_keys=False).apply(add_prop)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "namesap" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "При выполнении такой операции группировки часто бывает полезно произвести проверку разумности результата, например удостовериться, что сумма значений в столбце _prop_ по всем группам равна 1." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "namesap.groupby([\"year\", \"sex\"])[\"prop\"].sum()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Далее получим подмножество данных, чтобы упростить последующий анализ: первые 1000 имен для каждой комбинации пола и  года. Это еще одна групповая операция" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def get_top1000(group):\n", " return group.sort_values(\"births\", ascending=False)[:1000]\n", "\n", "top1000 = namesap.groupby([\"year\", \"sex\"]).apply(get_top1000)\n", "top1000.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Это набор, содержащий первые 1000 записей. Его мы и будем использовать для исследования данных в дальнейшем." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "top1000 = top1000.reset_index(drop=True)\n", "top1000.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Имея полный набор данных и первые 1000 записей, можно приступить к анализу различных интересных тенденций. \n", "\n", "Для начала решим простую задачу: разобьем набор _top1000_ на части, относящиеся к мальчикам и девочкам." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "boys = top1000[top1000[\"sex\"] == \"M\"]\n", "girls = top1000[top1000[\"sex\"] == \"F\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Можно вывести на график временные ряды, например, количество Джонов и Мэри в каждом году, но для этого потребуется предварительное переформатирование. \n", "Сформируем сводную таблицу, в которой представлено общее число родившихся по годам и именам" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "total_births = top1000.pivot_table(\"births\", index=\"year\", columns=\"name\", aggfunc=sum)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь можно нанести на график несколько имен, воспользовавшись методом _plot_ объекта _DataFrame_" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "total_births.info()\n", "subset = total_births[[\"John\", \"Harry\", \"Mary\", \"Marilyn\"]]\n", "subset.plot(subplots=True, figsize=(12, 10), title=\"Число рождений в год\")\n", "pass" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Глядя на графики, можно сделать вывод, что эти имена в Америке вышли из моды, но на самом деле картина несколько сложнее" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Убывание кривых на рисунках выше можно объяснить тем, что меньше родителей стали выбирать такие распространенные имена. Эту гипотезу можно проверить и подтвердить имеющимися данными. Один из возможных показателей – доля родившихся в наборе 1000 самых популярных имен, который агрегируем по году и полу" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "table = top1000.pivot_table(\"prop\", index=\"year\", columns=\"sex\", aggfunc=sum)\n", "table.plot(title=\"Сумма доли 1000 имен (table1000.prop) по году и полу\",\n", " yticks=np.linspace(0, 1.2, 13))\n", "pass" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Действительно, похоже, что разнообразие имен растет (доля в первой тысяче падает). \n", "\n", "Другой интересный показатель – количество различных имен среди первых 50 % родившихся, упорядоченное по популярности в порядке убывания. Вычислить его несколько сложнее. Рассмотрим только имена мальчиков, родившихся в 2000 году" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df = boys[boys[\"year\"] == 2000]\n", "df" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "После сортировки _prop_ в порядке убывания мы хотим узнать, сколько популярных имен нужно взять, чтобы достичь 50 %. \n", "\n", "Можно написать для этого цикл _for_, но _NumPy_ предлагает более хитроумный векторный подход:\n", "\n", "Если вычислить накопительные суммы _cumsum_ массива _prop_, а затем вызвать метод _searchsorted_, то будет возвращена позиция в массиве накопительных сумм, в которую нужно было бы вставить 0.5, чтобы не нарушить порядок сортировки" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "prop_cumsum = df[\"prop\"].sort_values(ascending=False).cumsum()\n", "prop_cumsum[:10]\n", "prop_cumsum.searchsorted(0.5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Поскольку индексация массивов начинается с  нуля, то нужно прибавить к результату 1 и получится 77. Заметим, что в 1900 году этот показатель был гораздо меньше" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df = boys[boys.year == 1900]\n", "in1900 = df.sort_values(\"prop\", ascending=False).prop.cumsum()\n", "in1900.searchsorted(0.5) + 1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь можно применить данную операцию к  каждой комбинации года и пола; произведем группировку по этим полям с помощью метода _groupby_, а затем с помощью метода _apply_ применим функцию, возвращающую счетчик для каждой группы" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def get_quantile_count(group, q=0.5):\n", " group = group.sort_values(\"prop\", ascending=False)\n", " return group.prop.cumsum().searchsorted(q) + 1\n", "# оценим разнообразие\n", "diversity = top1000.groupby([\"year\", \"sex\"]).apply(get_quantile_count)\n", "# преобразуем series во фрейм\n", "diversity = diversity.unstack()\n", "diversity.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В получившемся объекте _DataFrame_ с именем _diversity_ хранится два временных ряда, по одному для каждого пола, индексированные по году. \n", "\n", "Его выведем на график" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#\n", "fig = plt.figure()\n", "diversity.plot(title=\"Число популярных имен в 50%\")\n", "pass" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как видим, девочкам всегда давали более разнообразные имена, чем мальчикам, и со временем эта тенденция проявляется все ярче. \n", "\n", "Для анализа того, что именно является причиной разнообразия (например, растет число вариантов написания одного и того же имени), нужно сделать доп.разбор" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "!!!***!!!\n", "\n", "В 2007 году исследовательница детских имен Лаура Уоттенберг (Laura Wattenberg) отметила на своем сайте (http://www.babynamewizard.com), что распределение имен мальчиков по последней букве за последние 100 лет существенно изменилось. \n", "Чтобы убедиться в этом, сначала агрегируем данные полного набора обо всех родившихся по году, полу и последней букве" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def get_last_letter(x):\n", " return x[-1]\n", "\n", "last_letters = names[\"name\"].map(get_last_letter)\n", "last_letters.name = \"last_letter\"\n", "\n", "table = names.pivot_table(\"births\", index=last_letters,\n", " columns=[\"sex\", \"year\"], aggfunc=sum)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Затем выберем из всего периода три репрезентативных года и напечатаем первые несколько строк" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "subtable = table.reindex(columns=[1910, 1960, 2000], level=\"year\")\n", "subtable.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Далее пронормируем эту таблицу на общее число родившихся, чтобы вычислить новую таблицу, содержащую долю от общего количества родившихся для каждого пола и каждой последней буквы" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "subtable.sum()\n", "letter_prop = subtable / subtable.sum()\n", "letter_prop" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Зная доли букв, теперь можно нарисовать столбчатые диаграммы для каждого пола, разбив их по годам" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#import matplotlib.pyplot as plt\n", "# Доли имен мальчиков и девочек, заканчивающихся на каждую букву\n", "fig, axes = plt.subplots(2, 1, figsize=(10, 9))\n", "plt.subplots_adjust(hspace=0.25)\n", "letter_prop[\"M\"].plot(kind=\"bar\", rot=0, ax=axes[0], title=\"мальчики\", xlabel = \"последняя буква в имени\")\n", "letter_prop[\"F\"].plot(kind=\"bar\", rot=0, ax=axes[1], title=\"девочки\", xlabel = \"последняя буква в имени\", legend=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как видим, с 1960-х годов доля имен мальчиков, заканчивающихся буквой n, значительно возросла. \n", "\n", "Вернемся к созданной ранее полной таблице, пронормируем ее по году и полу, выберем некое подмножество букв для имен мальчиков, и транспонируем, чтобы превратить каждый столбец во временной ряд" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "letter_prop = table / table.sum()\n", "# выборка и транспонирование\n", "dny_ts = letter_prop.loc[[\"d\", \"n\", \"y\"], \"M\"].T\n", "dny_ts.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Имея этот объект _DataFrame_, содержащий временные ряды, можно методом _plot_ построить график изменения тенденций в  зависимости от времени" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.close(\"all\")\n", "fig = plt.figure()\n", "dny_ts.plot(title=\"Изменение доли мальчиков с именами,заканчивающимися на буквы d,n,y во времени\", xlabel = \"годы\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "!!!+++!!!\n", "Еще одно интересное наблюдение – изучить имена, которые раньше чаще давали мальчикам, а  затем «сменили пол». \n", "\n", "Возьмем, например, имя Lesley или Leslie. По набору top1000 вычислим список имен, начинающихся с 'lesl'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "all_names = pd.Series(top1000[\"name\"].unique())\n", "lesley_like = all_names[all_names.str.contains(\"Lesl\")]\n", "lesley_like" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Далее можно оставить только эти имена и просуммировать количество родившихся, сгруппировав по имени, чтобы найти относительные частоты" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "filtered = top1000[top1000[\"name\"].isin(lesley_like)]\n", "filtered.groupby(\"name\")[\"births\"].sum()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Затем агрегируем по полу и  году и нормируем в пределах каждого года" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tableL = filtered.pivot_table(\"births\", index=\"year\", columns=\"sex\", aggfunc=\"sum\")\n", "tableL = tableL.div(tableL.sum(axis=\"columns\"), axis=\"index\")\n", "tableL.tail()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь нетрудно построить график распределения по полу в  зависимости от времени" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig = plt.figure()\n", "tableL.plot(style={\"M\": \"k-\", \"F\": \"k--\"},\n", " title= \"Изменение во времени доли мальчиков и девочек с именами, похожими на Lesley\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Найдите и проверьте изменение популярности имен типа Mu(o)hammad, Mu(o)hammed, Mu(o)hamed, Mu(o)hamad" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.7" } }, "nbformat": 4, "nbformat_minor": 4 }