В заключительной главе основной части книги мы рассмотрим некоторые наборы данных из реального мира. Для каждого набора данных мы будем использовать методы, представленные ранее, чтобы извлечь значимую информацию из исходных данных. Эти методы применимы и к другим наборам данных, включая ваши собственные. В этой главе представлены разнообразные примеры наборов данных, которые можно использовать для практики.
Примеры наборов данных можно найти в репозитории Github, как описано в первой главе.
В 2011 году сервис сокращения URL-адресов Bitly сотрудничал с правительственным сайтом США USA.gov, предоставляя анонимные данные, собранные от пользователей, создающих короткие ссылки на .gov или .mil сайты. В 2011 году, помимо данных в реальном времени, можно было загрузить текстовые файлы с часовыми снимками. На момент написания этой книги (2017 год) этот сервис уже был закрыт, но мы сохранили данные для использования в качестве примера в этой книге.
Рассмотрим пример часового снимка, где каждая строка имеет формат JSON (JavaScript Object Notation), который является распространённым форматом данных для веб-приложений. Например, если мы прочитаем только первую строку файла, то увидим следующее:
In [5]: path = 'datasets/bitly_usagov/example.txt'
In [6]: open(path).readline()
Out[6]: '{ "a": "Mozilla\\/5.0 (Windows NT 6.1; WOW64) AppleWebKit\\/535.11
(KHTML, like Gecko) Chrome\\/17.0.963.78 Safari\\/535.11", "c": "US", "nk": 1,
"tz": "America\\/New_York", "gr": "MA", "g": "A6qOVH", "h": "wfLQtf", "l":
"orofrog", "al": "en-US,en;q=0.8", "hh": "1.usa.gov", "r":
"http:\\/\\/www.facebook.com\\/l\\/7AQEFzjSi\\/1.usa.gov\\/wfLQtf", "u":
"http:\\/\\/www.ncbi.nlm.nih.gov\\/pubmed\\/22415991", "t": 1331923247, "hc":
1331822918, "cy": "Danvers", "ll": [ 42.576698, -70.954903 ] }\n'
Python предоставляет встроенные или сторонние модули для преобразования строк JSON в объекты Python Dictionary. Здесь я буду использовать модуль json и его функцию loads для загрузки данных из файла по одной строке за раз:
import json
path = 'datasets/bitly_usagov/example.txt'
records = [json.loads(line) for line in open(path)]
Теперь объект records представляет собой список объектов Python Dictionary:
In [18]: records[0]
Out[18]:
{'a': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko)
Chrome/17.0.963.78 Safari/535.11',
'al': 'en-US,en;q=0.8',
'c': 'US',
'cy': 'Danvers',
'g': 'A6qOVH',
'gr': 'MA',
'h': 'wfLQtf',
'hc': 1331822918,
'hh': '1.usa.gov',
'l': 'orofrog',
'll': [42.576698, -70.954903],
'nk': 1,
'r': 'http://www.facebook.com/l/7AQEFzjSi/1.usa.gov/wfLQtf',
't': 1331923247,
'tz': 'America/New_York',
'u': 'http://www.ncbi.nlm.nih.gov/pubmed/22415991'}
Предположим, мы хотим узнать, какой часовой пояс наиболее часто встречается в этом наборе данных (то есть поле tz). Существует множество способов решить эту задачу. Во-первых, мы можем использовать список для извлечения всех часовых поясов:
In [12]: time_zones = [rec['tz'] for rec in records]
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-12-db4fbd348da9> in <module>()
----> 1 time_zones = [rec['tz'] for rec in records]
<ipython-input-12-db4fbd348da9> in <listcomp>(.0)
----> 1 time_zones = [rec['tz'] for rec in records]
KeyError: 'tz'
О, нет! Оказывается, не все записи содержат поле часового пояса. Это легко исправить, добавив проверку существования поля 'tz' перед извлечением значения:
In [13]: time_zones = [rec['tz'] for rec in records if 'tz' in rec]
In [14]: time_zones[:10]
Out[14]:
['America/New_York',
'America/Denver',
'America/New_York',
'America/Sao_Paulo',
'America/New_York',
'America/New_York',
'Europe/Warsaw',
'',
'',
'']
Просмотрев первые 10 часовых поясов, мы видим, что некоторые из них не определены (пустые строки). Хотя их можно исключить, пока мы оставим их. Теперь, чтобы подсчитать количество появлений каждого часового пояса, мы представим два метода: один более сложный (использующий только стандартные библиотеки Python), а другой более простой (с использованием pandas). Один из способов подсчёта — это сохранение счётчика в словаре во время перебора часовых поясов:
def get_counts(sequence):
counts = {}
for x in sequence:
if x in counts:
counts[x] += 1
else:
counts[x] = 1
return counts
Если вы используете более продвинутые инструменты из стандартной библиотеки Python, вы можете написать код более лаконично:
from collections import defaultdict
def get_counts2(sequence):
counts = defaultdict(int) # значения будут инициализированы до 0
for x in sequence:
counts[x] += 1
return counts
Я поместил логику в функцию для повышения возможности повторного использования. Чтобы применить её к часовым поясам, просто передайте time_zones:
In [17]: counts = get_counts(time_zones)
In [18]: counts['America/New_York']
Out[18]: 1251
In [19]: len(time_zones)
Out[19]: 3440
Чтобы получить первые 10 часовых поясов и их количество, нам нужно использовать некоторые приёмы работы со словарями:
def top_counts(count_dict, n=10):
value_key_pairs = [(count, tz) for tz, count in count_dict.items()]
value_key_pairs.sort()
return value_key_pairs[-n:]
Затем:
In [21]: top_counts(counts)
Out[21]:
[(33, 'America/Sao_Paulo'),
(35, 'Europe/Madrid'),
(36, 'Pacific/Honolulu'),
(37, 'Asia/Tokyo'),
(74, 'Europe/London'),
(191, 'America/Denver'),
(382, 'America/Los_Angeles'),
(400, 'America/Chicago'),
(521, ''),
(1251, 'America/New_York')]
Если вы ищете в стандартной библиотеке Python, вы найдёте класс Counter из модуля collections, который может упростить эту работу:
In [22]: from collections import Counter
In [23]: counts = Counter(time_zones)
In [24]: counts.most_common(10)
Out[24]: **Текст запроса:**
np.where(cframe['a'].str.contains('Windows'), 'Windows', 'Not Windows')
In [48]: cframe['os'][:5]
Out[48]:
0 Windows
1 Not Windows
2 Windows
3 Not Windows
4 Windows
Name: os, dtype: object
**Перевод текста запроса:**
НП. где (cframe ['a']. str. содержит ('Windows')), 'Windows', 'Не Windows')
В [48]: кадр ['os'] [: 5]
Выход [48]:
0 Windows
1 Не Windows
2 Windows
3 Не Windows
4 Windows
Имя: os, тип данных: объект
**Продолжения перевода нет, так как в запросе отсутствует продолжение исходного текста.**
*Если у вас есть дополнительные вопросы или уточнения по переводу, пожалуйста, дайте мне знать.* **Анализ предпочтений зрителей: какие фильмы больше всего нравятся женщинам и мужчинам**
Чтобы узнать, какие фильмы больше всего нравятся женской аудитории, можно отсортировать данные по столбцу «F» в порядке убывания:
Young Guns (1988) 3.371795 3.425620 Young Guns II (1990) 2.934783 2.904025 Young Sherlock Holmes (1985) 3.514706 3.363344 Zero Effect (1998) 3.864407 3.723140 eXistenZ (1999) 3.098592 3.289086 [1216 rows x 2 columns]
Для того чтобы понять, какой фильм больше всего нравится женской аудитории, а какой — мужской, можно добавить столбец с разницей средних оценок для каждого фильма и отсортировать по нему:
```python
In [87]: mean_ratings['diff'] = mean_ratings['M'] - mean_ratings['F']
Close Shave, A (1995) 4.644444 4.473795
Wrong Trousers, The (1993) 4.588235 4.478261
Sunset Blvd. (a.k.a. Sunset Boulevard) (1950) 4.572650 4.464589
Wallace & Gromit: The Best of Aardman Animation... 4.563107 4.385075
Schindler's List (1993) 4.562602 4.491415
Shawshank Redemption, The (1994) 4.539075 4.560625
Grand Day Out, A (1992) 4.537879 4.293255
To Kill a Mockingbird (1962) 4.536667 4.372611
Creature Comforts (1990) 4.513889 4.272277
Usual Suspects, The (1995) 4.513317 4.518248
Если отсортировать список по столбцу diff в порядке возрастания, то можно увидеть, что фильмы с наибольшей разницей в оценках между мужчинами и женщинами больше нравятся женщинам. Если же отсортировать список в обратном порядке, то получится список фильмов, которые больше нравятся мужской аудитории.
Также можно вычислить стандартное отклонение оценок для каждого фильма, чтобы найти фильм с наибольшим разбросом оценок. 1.259624 Эвита (1996) 1.253631 Билли Мэдисон (1995) 1.249970 Страх и ненависть в Лас-Вегасе (1998) 1.246408 Двухсотлетний человек (1999) 1.245533 Название: рейтинг, тип данных: float64
Возможно, вы уже заметили, что классификация фильмов представлена в виде строк, разделённых вертикальной чертой (|). Если вы хотите провести анализ классификации фильмов, то сначала необходимо преобразовать её в более удобный формат.
14.3. Имена детей в США с 1880 по 2010 год
Американское управление социального обеспечения (SSA) предоставило данные о частоте имён детей с 1880 года до настоящего времени. Хадли Уикхэм (автор многих популярных пакетов R) часто использует эти данные для демонстрации возможностей обработки данных в R.
Нам нужно выполнить некоторую очистку данных, чтобы загрузить этот набор данных. Это создаст следующий DataFrame:
In [4]: names.head(10)
Out[4]:
name sex births year
0 Mary F 7065 1880
1 Anna F 2604 1880
2 Emma F 2003 1880
3 Elizabeth F 1939 1880
4 Minnie F 1746 1880
5 Margaret F 1578 1880
6 Ida F 1472 1880
7 Alice F 1414 1880
8 Bertha F 1320 1880
9 Sarah F 1288 1880
С этим набором данных можно сделать многое, например:
Используя инструменты, описанные ранее, все эти аналитические задачи можно легко выполнить, и я объясню некоторые из них.
К моменту написания этой книги SSA разделило базу данных на несколько файлов по годам, предоставляя общее количество рождений для каждой комбинации пола/имени. Эти файлы можно получить в исходном формате здесь: http://www.ssa.gov/oact/babynames/limits.html.
Если эта страница больше не доступна, когда вы читаете эту книгу, вы можете использовать поисковые системы для поиска.
Скачайте файл «National data» names.zip, разархивируйте его, и в каталоге вы найдёте набор файлов (например, yob1880.txt). Я использовал команду UNIX head для просмотра первых 10 строк одного из файлов (в Windows вы можете использовать команду more или открыть текстовый редактор напрямую):
In [94]: !head -n 10 datasets/babynames/yob1880.txt
Mary,F,7065
Anna,F,2604
Emma,F,2003
Elizabeth,F,1939
Minnie,F,1746
Margaret,F,1578
Ida,F,1472
Alice,F,1414
Bertha,F,1320
Sarah,F,1288
Поскольку это очень стандартный формат с разделением запятыми, мы можем использовать pandas.read_csv для загрузки его в DataFrame:
In [95]: import pandas as pd
In [96]: names1880 =
pd.read_csv('datasets/babynames/yob1880.txt',
....: names=['name', 'sex', 'births'])
In [97]: names1880
Out[97]:
name sex births
0 Mary F 7065
1 Anna F 2604
2 Emma F 2003
3 Elizabeth F 1939
4 Minnie F 1746
... ... .. ...
1995 Woodie M 5
1996 Worthy M 5
1997 Wright M 5
1998 York M 5
1999 Zachariah M 5
[2000 rows x 3 columns]
Эти файлы содержат только имена, которые появились более 5 раз в этом году. Для простоты мы можем использовать столбец sex для группировки и суммирования значений births:
In [98]: names1880.groupby('sex').births.sum()
Out[98]:
sex
F 90993
M 110493
Name: births, dtype: int64
Так как набор данных разделён на отдельные файлы по годам, первая задача — объединить все данные в один DataFrame и добавить столбец year. Мы можем использовать функцию pandas.concat для достижения этой цели:
years = range(1880, 2011)
pieces = []
columns = ['name', 'sex', 'births']
for year in years:
path = 'datasets/babynames/yob%d.txt' % year
frame = pd.read_csv(path, names=columns)
frame['year'] = year
pieces.append(frame)
# Concatenate everything into a single DataFrame
names = pd.concat(pieces, ignore_index=True)
Здесь следует обратить внимание на несколько моментов. Во-первых, concat по умолчанию объединяет несколько DataFrames построчно; во-вторых, необходимо указать ignore_index = True, поскольку мы не хотим сохранять исходный номер строки, возвращаемый read_csv. Теперь у нас есть большой DataFrame, содержащий все данные об именах:
In [100]: names
Out[100]:
name sex births year
0 Mary F 7065 1880
1 Anna F 2604 1880
2 Emma F 2003 1880
3 Elizabeth F 1939 1880
4 Minnie F 1746 1880
... ... .. ... ...
1690779 Zymaire M 5 2010
1690780 Zyonne M 5 2010
1690781 Zyquarius M 5 2010
1690782 Zyran M 5 2010
1690783 Zzyzx M 5 2010
[1690784 rows x 4 columns]
Имея эти данные, мы теперь можем использовать groupby или pivot_table для агрегирования данных на уровне year и sex, как показано на рисунке 14-4:
In [101]: total_births = names.pivot_table('births', index='year',
.....: columns='sex', aggfunc=sum)
In [102]: total_births.tail()
Out[102]:
sex F M
year
2006 1896468 2050234
2007 1916888 2069242
2008 1883645 2032310
2009 1827643 1973359
2010 1759010 1898382
In [103]: total_births.plot(title='Total births by sex and year')
Рисунок 14–4 Общее количество рождений по полу и году Ниже мы вставляем столбец prop, чтобы хранить долю количества детей с определённым именем относительно общего числа новорождённых. Значение prop равное 0.02 означает, что среди 100 новорождённых двое получили это имя. Поэтому сначала мы группируем данные по годам и полу, а затем добавляем новый столбец к каждой группе:
def add_prop(group):
group['prop'] = group.births / group.births.sum()
return group
names = names.groupby(['year', 'sex']).apply(add_prop)
Теперь полный набор данных имеет следующие столбцы:
In [105]: names
Out[105]:
name sex births year prop
0 Mary F 7065 1880 0.077643
1 Anna F 2604 1880 0.028618
2 Emma F 2003 1880 0.022013
3 Elizabeth F 1939 1880 0.021309
4 Minnie F 1746 1880 0.019188
... ... .. ... ... ...
1690779 Zymaire M 5 2010 0.000003
1690780 Zyonne M 5 2010 0.000003
1690781 Zyquarius M 5 2010 0.000003
1690782 Zyran M 5 2010 0.000003
1690783 Zzyzx M 5 2010 0.000003
[1690784 rows x 5 columns]
При выполнении такой групповой обработки обычно следует проводить некоторые проверки эффективности, например, проверять, равна ли сумма всех prop для каждой группы 1:
In [106]: names.groupby(['year', 'sex']).prop.sum()
Out[106]:
year sex
1880 F 1.0
M 1.0
1881 F 1.0
M 1.0
1882 F 1.0
...
2008 M 1.0
2009 F 1.0
M 1.0
2010 F 1.0
M 1.0
Name: prop, Length: 262, dtype: float64
Работа выполнена. Чтобы облегчить дальнейший анализ, мне нужно извлечь подмножество данных: первые 1000 имён для каждой пары sex/year. Это ещё одна групповая операция:
def get_top1000(group):
return group.sort_values(by='births', ascending=False)[:1000]
grouped = names.groupby(['year', 'sex'])
top1000 = grouped.apply(get_top1000)
# Drop the group index, not needed
top1000.reset_index(inplace=True, drop=True)
Если вам нравится DIY, вы также можете сделать это следующим образом:
pieces = []
for year, group in names.groupby(['year', 'sex']):
pieces.append(group.sort_values(by='births', ascending=False)[:1000])
top1000 = pd.concat(pieces, ignore_index=True)
Теперь результат данных выглядит так:
In [108]: top1000
Out[108]:
name sex births year prop
0 Mary F 7065 1880 0.077643
1 Anna F 2604 1880 0.028618
2 Emma F 2003 1880 0.022013
3 Elizabeth F 1939 1880 0.021309
4 Minnie F 1746 1880 0.019188
... ... .. ... ... ...
261872 Camilo M 194 2010 0.000102
261873 Destin M 194 2010 0.000102
261874 Jaquan M 194 2010 0.000102
261875 Jaydan M 194 2010 0.000102
261876 Maxton M 193 2010 0.000102
[261877 rows x 5 columns]
Следующий этап анализа данных будет сосредоточен на этом наборе top1000 данных.
Имея полный набор данных и только что созданный набор top1000, мы можем начать анализировать различные тенденции именования. Сначала разделим первые 1000 имен на две части: мужскую и женскую:
In [109]: boys = top1000[top1000.sex == 'M']
In [110]: girls = top1000[top1000.sex == 'F']
Это два простых временных ряда, которые можно легко построить после небольшого упорядочивания (например, количество детей, названных John и Mary каждый год). Сначала мы создаём сводную таблицу, которая подсчитывает общее количество рождений для каждого имени и года:
In [111]: total_births = top1000.pivot_table('births', index='year',
.....: columns='name',
.....: aggfunc=sum)
Теперь мы используем метод plot DataFrame для построения графиков нескольких имён (см. рисунок 14-5):
In [112]: total_births.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 131 entries, 1880 to 2010
Columns: 6868 entries, Aaden to Zuri
dtypes: float64(6868)
memory usage: 6.9 MB
In [113]: subset = total_births[['John', 'Harry', 'Mary', 'Marilyn']]
In [114]: subset.plot(subplots=True, figsize=(12, 10), grid=False,
.....: title="Number of births per year")
Из графика видно, что эти имена больше не популярны среди американцев. Однако дело обстоит не так просто, и в следующем разделе мы узнаем, как это произошло.
Одно из возможных объяснений заключается в том, что родители всё меньше хотят давать своим детям популярные имена. Эту гипотезу можно проверить с помощью данных. Один из способов — рассчитать долю самых популярных 1000 имён и построить график по годам и полам:
In [116]: table = top1000.pivot_table('prop', index='year',
.....: columns='sex', aggfunc=sum)
In [117]: table.plot(title='Sum of table1000.prop by year and sex',
.....: yticks=np.linspace(0, 1.2, 13), xticks=range(1880, 2020, 10)
)
График показывает, что разнообразие имён действительно увеличилось (доля первых 1000 имён снизилась). Другой способ — подсчитать количество различных имён, составляющих половину от общего количества рождений, но этот расчёт сложен. Мы рассмотрим только имена мальчиков 2010 года:
In [118]: df **В приведённом тексте рассматриваются различные методы работы с данными и их анализ.**
В запросе используется язык Python.
**Текст содержит фрагменты кода, которые оставлены без перевода.**
= boys[boys.year == 2010]
In [119]: df
Out[119]:
name sex births year prop
260877 Jacob M 21875 2010 0.011523
260878 Ethan M 17866 2010 0.009411
260879 Michael M 17133 2010 0.009025
260880 Jayden M 17030 2010 0.008971
260881 William M 16870 2010 0.008887
... ... .. ... ... ...
261872 Camilo M 194 2010 0.000102
261873 Destin M 194 2010 0.000102
261874 Jaquan M 194 2010 0.000102
261875 Jaydan M 194 2010 0.000102
261876 Maxton M 193 2010 0.000102
[1000 rows x 5 columns]
После сортировки по prop в порядке убывания мы хотим узнать, сколько имён нужно сложить вместе, чтобы получить 50%. Хотя можно написать цикл for для достижения этой цели, NumPy предлагает более умный векторный способ. Сначала мы вычисляем совокупную сумму prop с помощью метода cumsum(), а затем используем метод searchsorted() для поиска позиции, где 0,5 должен быть вставлен, чтобы сохранить порядок:
In [120]: prop_cumsum = df.sort_values(by='prop', ascending=False).prop.cumsum()
In [121]: prop_cumsum[:10]
Out[121]:
260877 0.011523
260878 0.020934
260879 0.029959
260880 0.038930
260881 0.047817
260882 0.056579
260883 0.065155
260884 0.073414
260885 0.081528
260886 0.089621
Name: prop, dtype: float64
In [122]: prop_cumsum.values.searchsorted(0.5)
Out[122]: 116
Поскольку массив индексируется начиная с 0, мы должны добавить 1 к этому результату, что даёт нам окончательный результат 117. Для сравнения, если мы возьмём данные за 1900 год, этот номер будет намного меньше:
In [123]: df = boys[boys.year == 1900]
In [124]: in1900 = df.sort_values(by='prop', ascending=False).prop.cumsum()
In [125]: in1900.values.searchsorted(0.5) + 1
Out[125]: 25
Теперь мы можем выполнить этот расчёт для всех комбинаций year/sex. Мы группируем данные по этим полям, а затем применяем функцию для вычисления значения для каждой группы:
def get_quantile_count(group, q=0.5):
group = group.sort_values(by='prop', ascending=False)
return group.prop.cumsum().values.searchsorted(q) + 1
diversity = top1000.groupby(['year', 'sex']).apply(get_quantile_count)
diversity = diversity.unstack('sex')
Сейчас diversity — это DataFrame с двумя временными рядами (по одному для каждого пола, проиндексированному по годам). С помощью IPython вы можете просмотреть его содержимое и построить график (например, рисунок 14-7):
In [128]: diversity.head()
Out[128]:
sex F M
year
1880 38 14
1881 38 14
1882 38 15
1883 39 15
1884 39 16
In [129]: diversity.plot(title="Number of popular names in top 50%")
Из графика видно, что разнообразие женских имён всегда выше, чем мужских, и продолжает расти. Читатели могут сами проанализировать, какие факторы способствуют этому разнообразию (например, изменения в написании).
В 2007 году исследователь имён Laura Wattenberg на своём веб-сайте отметила (http://www.babynamewizard.com), что распределение мужских имён по последней букве значительно изменилось за последние сто лет. Чтобы понять ситуацию, я сначала объединил все данные о рождении по годам, полу и последней букве:
# extract last letter from name column
get_last_letter = lambda x: x[-1]
last_letters = names.name.map(get_last_letter)
last_letters.name = 'last_letter'
table = names.pivot_table('births', index=last_letters,
columns=['sex', 'year'], aggfunc=sum)
Затем я выбрал три репрезентативных года и вывел первые несколько строк:
In [131]: subtable = table.reindex(columns=[1910, 1960, 2010], level='year')
In [132]: subtable.head()
Out[132]:
sex F M
year 1910 1960 2010 1910 1960 2010
last_letter
a 108376.0 691247.0 670605.0 977.0 5204.0 28438.0
b NaN 694.0 450.0 411.0 3912.0 38859.0
c 5.0 49.0 946.0 482.0 15476.0 23125.0
d 6750.0 3729.0 2607.0 22111.0 262112.0 44398.0
e 133569.0 435013.0 313833.0 28655.0 178823.0 129012.0
Далее мы нормализуем таблицу, чтобы вычислить долю каждой буквы в общем количестве рождений:
In [133]: subtable.sum()
Out[133]:
sex year
F 1910 396416.0
1960 2022062.0
2010 1759010.0
M 1910 194198.0
1960 2132588.0
2010 1898382.0
dtype: float64
In [134]: letter_prop = subtable / subtable.sum()
In [135]: letter_prop
Out[135]:
sex F M
year 1910 1960 2010 1910 1960 2010
last_letter
a 0.273390 0.341853 0.381240 0.005031 0.002440 0.014980
b NaN 0.000343 0.000256 0.002116 0.001834 0.020470
c 0.000013 0.000024 0.000538 **Имеющиеся данные представляют собой текст на языке Python.**
В запросе содержатся фрагменты кода, которые невозможно перевести без контекста.
**Текст запроса:**
```python
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 1, figsize=(10, 8))
letter_prop['M'].plot(kind='bar', rot=0, ax=axes[0], title='Male')
letter_prop['F'].plot(kind='bar', rot=0, ax=axes[1], title='Female',
legend=False)
Перевод:
Импортируем модуль matplotlib.pyplot
как plt
.
Создаём переменные fig
и axes
, присваивая им значения, возвращаемые методом subplots()
модуля pyplot
, который импортировали. Метод принимает три аргумента: количество строк (2), количество столбцов (1), размер фигуры (10 на 8).
Вызываем метод plot()
объекта letter_prop
с ключом 'M'
. В качестве аргументов передаём следующие параметры: тип графика — 'bar'
, значение поворота — 0
, ссылка на ось — axes[0]
, заголовок — 'Male'
.
Вызываем метод plot()
объекта letter_prop
с ключом 'F'
. В качестве аргументов передаём те же параметры, что и для предыдущего вызова метода plot()
, за исключением заголовка, который равен 'Female'
. Также добавляем параметр legend=False
. ```
non-null object
dtypes: int64(1), object(3)
memory usage: 207.5+ KB
In [173]: col_mapping = {'description' : 'nutrient', .....: 'group' : 'nutgroup'} In [174]: nutrients = nutrients.rename(columns=col_mapping, copy=False)
In [175]: nutrients Out[175]: nutrient nutgroup units value id 0 Protein Composition g 25.180 1008 1 Total lipid (fat) Composition g 29.200 1008 2 Carbohydrate, by difference Composition g 3.060 1008 3 Ash Other g 3.280 1008 4 Energy Energy kcal 376.000 1008 ... ... ... ... ... ... 389350 Vitamin B-12, added Vitamins mcg 0.000 43546 389351 Cholesterol Other mg 0.000 43546 389352 Fatty acids, total saturated Other g 0.072 43546 389353 Fatty acids, total monounsaturated Other g 0.028 43546 389354 Fatty acids, total polyunsaturated Other g 0.041 43546 [375176 rows x 5 columns]
Сделав это, можно объединить данные `info` и `nutrients`:
```python
In [176]: ndata = pd.merge(nutrients, info, on='id', how='outer')
In [177]: ndata.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 375176 entries, 0 to 375175
Data columns (total 8 columns):
nutrient 375176 non-null object
nutgroup 375176 non-null object
units 375176 non-null object
value 375176 non-null float64
id 375176 non-null int64
food 375176 non-null object
fgroup 375176 non-null object
manufacturer 293054 non-null object
dtypes: float64(1), int64(1), object(6)
memory usage: 25.8+ MB
In [178]: ndata.iloc[30000]
Out[178]:
nutrient Glycine
nutgroup Amino Acids
units g
value 0.04
id 6158
food Soup, tomato bisque, canned, condensed
fgroup Soups, Sauces, and Gravies
manufacturer
Name: 30000, dtype: object
Теперь мы можем создать диаграмму средних значений (как показано на рисунке 14-11):
In [180]: result = ndata.groupby(['nutrient', 'fgroup'])['value'].quantile(0.5)
In [181]: result['Zinc, Zn'].sort_values().plot(kind='barh')
Немного подумав, мы сможем определить, какие продукты наиболее богаты различными питательными веществами:
by_nutrient = ndata.groupby(['nutgroup', 'nutrient'])
get_maximum = lambda x: x.loc[x.value.idxmax()]
get_minimum = lambda x: x.loc[x.value.idxmin()]
max_foods = by_nutrient.apply(get_maximum)[['value', 'food']]
# make the food a little smaller
max_foods.food = max_foods.food.str[:50]
Полученный DataFrame слишком велик, чтобы полностью его здесь привести. Здесь представлен только раздел «Amino Acids»:
In [183]: max_foods.loc['Amino Acids']['food']
Out[183]:
nutrient
Alanine Gelatins, dry powder, unsweetened
Arginine Seeds, sesame flour, low-fat
Aspartic acid Soy protein isolate
Cystine Seeds, cottonseed flour, low fat (glandless)
Glutamic acid Soy protein isolate
...
Serine Soy protein isolate, PROTEIN TECHNOLOGIES INTE...
Threonine Soy protein isolate, PROTEIN TECHNOLOGIES INTE...
Tryptophan Sea lion, Steller, meat with fat (Alaska Native)
Tyrosine Soy protein isolate, PROTEIN TECHNOLOGIES INTE...
Valine Soy protein isolate, PROTEIN TECHNOLOGIES INTE...
Name: food, Length: 19, dtype: object
Федеральный избирательный комитет США опубликовал данные о финансировании политических кампаний. Эти данные включают в себя информацию об именах спонсоров, их профессиях, работодателях, адресах и суммах взносов. Нас интересуют данные по выборам президента США в 2012 году (http://www.fec.gov/disclosurep/PDownload.do). В июне 2012 года я скачал набор данных размером 150 МБ (P00000001-ALL.csv), который мы загрузим с помощью pandas.read_csv: 23432 ИНФОРМАЦИОННЫЙ ЗАПРОС ОТ BEST EFFORTS 21138 ИНЖЕНЕР 14334 УЧИТЕЛЬ 13990 КОНСУЛЬТАНТ 13273 ПРОФЕССОР 12555 Имя: contbr_occupation, dtype: int64
Не трудно заметить, что многие профессии включают в себя одинаковые основные виды работ, либо существует несколько вариантов одного и того же. Следующий фрагмент кода может очистить некоторые из этих данных (отображение информации о профессии на другую). Обратите внимание, здесь ловко используется dict.get, который позволяет профессиям без отображения также «проходить»:
```python
occ_mapping = {
'ИНФОРМАЦИОННЫЙ ЗАПРОС BEST EFFORTS' : 'НЕ ПРЕДОСТАВЛЕНО',
'ИНФОРМАЦИОННЫЙ ЗАПРОС' : 'НЕ ПРЕДОСТАВЛЕНО',
'ИНФОРМАЦИОННЫЙ ЗАПРОС (BEST EFFORTS)' : 'НЕ ПРЕДОСТАВЛЕНО',
'CEO': 'CEO'
}
# Если отображение не предоставлено, вернуть x
f = lambda x: occ_mapping.get(x, x)
fec.contbr_occupation = fec.contbr_occupation.map(f)
Я также провёл аналогичную обработку информации о работодателе:
emp_mapping = {
'ИНФОРМАЦИОННЫЙ ЗАПРОС PER BEST EFFORTS' : 'НЕ ПРЕДОСТАВЛЕНО',
'ИНФОРМАЦИОННЫЙ ЗАПРОС' : 'НЕ ПРЕДОСТАВЛЕНО',
'SELF' : 'SELF-EMPLOYED',
'САМОЗАНЯТЫЙ' : 'САМОЗАНЯТЫЙ',
}
# Если сопоставление не предоставлено, вернуть x
f = lambda x: emp_mapping.get(x, x)
fec.contbr_employer = fec.contbr_employer.map(f)
Теперь вы можете использовать pivot_table для агрегирования данных по партиям и профессиям, а затем отфильтровать данные с общей суммой взносов менее 2 миллионов долларов:
In [201]: by_occupation = fec.pivot_table('contb_receipt_amt',
.....: index='contbr_occupation',
.....: columns='party', aggfunc='sum')
In [202]: over_2mm = by_occupation[by_occupation.sum(1) > 2000000]
In [203]: over_2mm
Out[203]:
party Democrat Republican
contbr_occupation
АДВОКАТ 11141982.97 7.477194e+06
CEO 2074974.79 4.211041e+06
КОНСУЛЬТАНТ 2459912.71 2.544725e+06
ИНЖЕНЕР 951525.55 1.818374e+06
ИСПОЛНИТЕЛЬНЫЙ 1355161.05 4.138850e+06
... ... ...
ПРЕЗИДЕНТ 1878509.95 4.720924e+06
ПРОФЕССОР 2165071.08 2.967027e+05
НЕДВИЖИМОСТЬ 528902.09 1.625902e+06
ВЫШЕЛ НА ПЕНСИЮ 25305116.38 2.356124e+07
САМООБЕСПЕЧИВАЕМЫЙ 672393.40 1.640253e+06
[17 строк x 2 столбца]
Эти данные можно представить в виде гистограммы (barh означает горизонтальную гистограмму, как показано на рисунке 14-12):
In [205]: over_2mm.plot(kind='barh')
Рисунок 14–12: Распределение общих взносов по различным профессиям для каждой партии
Возможно, вы также захотите узнать, какие профессии и предприятия имеют самые высокие общие взносы для Обамы и Ромни. Для этого мы сначала группируем кандидатов, а затем используем метод, аналогичный top, описанному в этой главе:
def get_top_amounts(group, key, n=5):
totals = group.groupby(key)['contb_receipt_amt'].sum()
return totals.nlargest(n)
Затем мы агрегируем данные по профессиям и работодателям:
In [207]: grouped = fec_mrbo.groupby('cand_nm')
In [208]: grouped.apply(get_top_amounts, 'contbr_occupation', n=7)
Out[208]:
cand_nm contbr_occupation
Obama, Barack RETIRED 25305116.38
ATTORNEY 11141982.97
INFORMATION REQUESTED 4866973.96
HOMEMAKER 4248875.80
PHYSICIAN 3735124.94
...
Romney, Mitt HOMEMAKER 8147446.22
ATTORNEY 5364718.82
PRESIDENT 2491244.89
EXECUTIVE 2300947.03
C.E.O. 1968386.11
Name: contb_receipt_amt, Length: 14, dtype: float64
In [209]: grouped.apply(get_top_amounts, 'contbr_employer', n=10)
Out[209]:
cand_nm contbr_employer
Obama, Barack RETIRED 22694358.85
SELF-EMPLOYED 17080985.96
NOT EMPLOYED 8586308.70
INFORMATION REQUESTED 5053480.37
HOMEMAKER 2605408.54
...
Romney, Mitt CREDIT SUISSE 281150.00
MORGAN STANLEY 267266.00
GOLDMAN SACH & CO. 238250.00
BARCLAYS CAPITAL 162750.00
H.I.G. CAPITAL 139500.00
Name: contb_receipt_amt, Length: 20, dtype: float64
``` **Сейчас можно разделить данные об Обаме и Ромни по именам кандидатов и диапазонам сумм пожертвований, чтобы получить гистограмму:**
```python
In [213]: grouped = fec_mrbo.groupby(['cand_nm', labels])
In [214]: grouped.size().unstack(0)
Out[214]:
cand_nm Obama, Barack Romney, Mitt
contb_receipt_amt
(0, 1] 493.0 77.0
(1, 10] 40070.0 3681.0
(10, 100] 372280.0 31853.0
(100, 1000] 153991.0 43357.0
(1000, 10000] 22284.0 26186.0
(10000, 100000] 2.0 1.0
(100000, 1000000] 3.0 NaN
(1000000, 10000000] 4.0 NaN
Из этих данных видно, что Обама получил гораздо больше небольших пожертвований. Можно также суммировать суммы пожертвований в каждом диапазоне и нормализовать их, чтобы визуализировать пропорции пожертвований разных размеров для каждого кандидата (см. рисунок 14-13):
In [216]: bucket_sums = grouped.contb_receipt_amt.sum().unstack(0)
In [217]: normed_sums = bucket_sums.div(bucket_sums.sum(axis=1), axis=0)
In [218]: normed_sums
Out[218]:
cand_nm Obama, Barack Romney, Mitt
contb_receipt_amt
(0, 1] 0.805182 0.194818
(1, 10] 0.918767 0.081233
(10, 100] 0.910769 0.089231
(100, 1000] 0.710176 0.289824
(1000, 10000] 0.447326 0.552674
(10000, 100000] 0.823120 0.176880
(100000, 1000000] 1.000000 NaN
(1000000, 10000000] 1.000000 NaN
In [219]: normed_sums[:-2].plot(kind='barh')
Рисунок 14–13: Пропорции пожертвований разного размера для обоих кандидатов.
Я исключил два самых больших диапазона, потому что они не являются индивидуальными пожертвованиями.
Можно провести множество других анализов и улучшений. Например, можно сгруппировать данные по имени жертвователя и почтовому индексу, чтобы найти людей, которые делали небольшие пожертвования несколько раз, и тех, кто делал одно или несколько крупных пожертвований. Я настоятельно рекомендую вам скачать эти данные и изучить их самостоятельно.
Группировка данных по кандидату и штату является стандартной операцией:
In [220]: grouped = fec_mrbo.groupby(['cand_nm', 'contbr_st'])
In [221]: totals = grouped.contb_receipt_am.sum().unstack(0).fillna(0)
In [222]: totals = totals[totals.sum(1) > 100000]
In [223]: totals[:10]
Out[223]:
cand_nm Obama, Barack Romney, Mitt
contbr_st
AK 281840.15 86204.24
AL 543123.48 527303.51
AR 359247.28 105556.00
AZ 1506476.98 1888436.23
CA 23824984.24 11237636.60
CO 2132429.49 1506714.12
CT 2068291.26 3499475.45
DC 4373538.80 1025137.50
DE 336669.14 82712.00
FL 7318178.58 8338458.81
Если разделить каждую строку на общую сумму пожертвований, то получится пропорция пожертвований каждого кандидата в каждом штате:
In [224]: percent = totals.div(totals.sum(1), axis=0)
In [225]: percent[:10]
Out[225]:
cand_nm Obama, Barack Romney, Mitt
contbr_st
AK 0.765778 0.234222
AL 0.507390 0.492610
AR 0.772902 0.227098
AZ 0.443745 0.556255
CA 0.679498 0.320502
CO 0.585970 0.414030
CT 0.371476 0.628524
DC 0.810113 0.189887
DE 0.802776 0.197224
FL 0.467417 0.532583
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )