Python for Data Analysis

image

ch02

来自bit.ly的1.usa.gov数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import json
path = 'E:\PYTHON\PydataProject\ch02\data\example.txt'
a = open(path).readline()
records = [json.loads(line) for line in open(path)]
for index,item in enumerate(records, 1):
print(index, item)
-----------------------------------------------
3559 {'a': 'GoogleProducer', 'c': 'US', 'nk': 0, 'tz': 'America/Los_Angeles', 'gr': 'CA', 'g': 'zjtI4X', 'h': 'zjtI4X', 'l': 'bitly', 'hh': '1.usa.gov', 'r': 'direct', 'u': 'http://www.ahrq.gov/qual/qitoolkit/', 't': 1331926847, 'hc': 1327528527, 'cy': 'Mountain View', 'll': [37.419201, -122.057404]}
3560 {'a': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; MS-RTC LM 8; .NET4.0C; .NET4.0E; .NET CLR 1.1.4322)', 'c': 'US', 'nk': 0, 'tz': 'America/New_York', 'gr': 'VA', 'g': 'qxKrTK', 'h': 'qxKrTK', 'l': 'bitly', 'al': 'en-US', 'hh': '1.usa.gov', 'r': 'http://t.co/OEEEvwjU', 'u': 'http://herndon-va.gov/Content/public_safety/Public_Information/weekly_reports.aspx?cnlid=1736', 't': 1331926849, 'hc': 1312897670, 'cy': 'Mc Lean', 'll': [38.935799, -77.162102]}

records[:1]
Out[8]:
[{'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'}]

列表推导式,后加 if 判断字段是否存在

1
2
3
4
5
6
7
8
9
time_zones = [rec['tz'] for rec in records if 'tz' in rec]

time_zones[:5]
Out[6]:
['America/New_York',
'America/Denver',
'America/New_York',
'America/Sao_Paulo',
'America/New_York']

对时区计数——pandas库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from collections import defaultdict
def get_counts(sequence):
counts = defaultdict(int)
for x in sequence:
counts[x] +=1
return counts

counts = get_counts(time_zones)
num = counts['America/Los_Angeles']

counts
Out[11]:
defaultdict(int,
{'': 521,
'Africa/Cairo': 3,
'Africa/Casablanca': 1,
'Africa/Ceuta': 2,
'Africa/Johannesburg': 1,
'Africa/Lusaka': 1,
'America/Anchorage': 5,
'America/Argentina/Buenos_Aires': 1
……})

num
Out[14]: 382

前十位的时区及其计数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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:]
top_counts(counts)

----------------------------------------------
top_counts(counts)
Out[25]:
[(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')]

使用collections.Counter类计数更加方便

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from collections import Counter
counts = Counter(time_zones)
counts.most_common(10)

------------------------------------------
counts.most_common(10)
Out[26]:
[('America/New_York', 1251),
('', 521),
('America/Chicago', 400),
('America/Los_Angeles', 382),
('America/Denver', 191),
('Europe/London', 74),
('Asia/Tokyo', 37),
('Pacific/Honolulu', 36),
('Europe/Madrid', 35),
('America/Sao_Paulo', 33)]

用pandas对时区进行计数

1
2
3
4
5
from pandas import DataFrame,Series
import pandas as pd
import numpy as np

frame = DataFrame(records)
  • frame[‘tz’]返回的Series对象,该对象有一个
  • value_counts方法
  • Series对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    frame['tz'][:10]
    tz_count = frame['tz'].value_counts()
    tz_count[:10]

    --------------------------------------
    tz_count[:10]
    Out[24]:
    America/New_York 1251
    521
    America/Chicago 400
    America/Los_Angeles 382
    America/Denver 191
    Europe/London 74
    Asia/Tokyo 37
    Pacific/Honolulu 36
    Europe/Madrid 35
    America/Sao_Paulo 33
    Name: tz, dtype: int64

matplotlib数据可视化

  • 先处理记录中未知或缺失的时区填上一个替代值

fillna函数可以替代缺失值NA,未知值(空字符串)则可以通过布尔型数组索引加以替换

1
tz_count[:10].plot(kind='barh',rot = 0)

image

  • split()
  • dropna()
1
2
3
4
5
6
7
8
9
10
11
12
13
results = Series([x.split()[0] for x in frame.a.dropna()]) 
results[:5]
results.value_counts()[:8]
Out[20]:
Mozilla/5.0 2594
Mozilla/4.0 601
GoogleMaps/RochesterNY 121
Opera/9.80 34
TEST_INTERNET_AGENT 24
GoogleProducer 21
Mozilla/6.0 5
BlackBerry8520/5.0.0.681 4
dtype: int64
  • cframe = frame[frame.a.notnull()]
1
2
3
4
cframe = frame[frame.a.notnull()]
operating_system =np.where(cframe['a'].str.contains('Windows'),'Windows','Not Windows')
operating_system[:5]
by_tz_os = cframe.groupby(['tz',operating_system])
  • size()
  • unstack()

    对分组结果进行计数,并利用unstack对计数结果进行重塑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    agg_counts = by_tz_os.size().unstack().fillna(0)

    agg_counts
    Out[19]:
    Not Windows Windows
    tz
    245.0 276.0
    Africa/Cairo 0.0 3.0
    Africa/Casablanca 0.0 1.0
    Africa/Ceuta 0.0 2.0
    Africa/Johannesburg 0.0 1.0
    Africa/Lusaka 0.0 1.0
    America/Anchorage 4.0 1.0
    America/Argentina/Buenos_Aires 1.0 0.0
    America/Argentina/Cordoba 0.0 1.0
    America/Argentina/Mendoza 0.0 1.0
    America/Bogota 1.0 2.0
    America/Caracas 0.0 1.0
    America/Chicago 115.0 285.0

最后,我们来选取最常出现的时区。为了达到这个目的,我根据agg_counts中的行数构造了一个间接索引数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 用于按升序排列
indexer = agg_counts.sum(1).argsort()
indexer[:10]
Out[26]:
tz
24
Africa/Cairo 20
Africa/Casablanca 21
Africa/Ceuta 92
Africa/Johannesburg 87
Africa/Lusaka 53
America/Anchorage 54
America/Argentina/Buenos_Aires 57
America/Argentina/Cordoba 26
America/Argentina/Mendoza 55

然后通过take按照这个顺序截取了最后10行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
count_subset = agg_counts.take(indexer)[-10:]
count_subset
Out[29]:
Not Windows Windows
tz
America/Sao_Paulo 13.0 20.0
Europe/Madrid 16.0 19.0
Pacific/Honolulu 0.0 36.0
Asia/Tokyo 2.0 35.0
Europe/London 43.0 31.0
America/Denver 132.0 59.0
America/Los_Angeles 130.0 252.0
America/Chicago 115.0 285.0
245.0 276.0
America/New_York 339.0 912.0

使用stacked = True 来生成一张堆积条形图。按Windows和非Windows用户统计的最常出现的时区。
image

这张图不太容易看清楚小分组中Windows用户的相对比例,因此我们可以将各行规范化为“总计为1”并重新绘图。

1
2
normal_subset = count_subset.div(count_subset.sum(1),axis=0)
normal_subset.plot(kind='barh',stacked=True)

按Windows和非Windows用户比例统计的最常出现的时区。

image

MovieLens 1M 数据集

MovieLens 1M 数据集包含来自6000名用户对4000部电影的100万条评分数据。它分为三个表:评分、用户信息和电影信息。将该数据从zip文件中解压出来之后,可以通过pandas.read_table将各个表分别读到一个pandas DataFrame对象中:

数据导入

1
2
3
4
5
6
7
8
9
10
11
12
13
import pandas as pd 
import os

os.chdir('/Users/yueqiang/Python Coding/python data analysis data/datasets/')

unames = ['user_id','gender','age','occupation','zip']
users = pd.read_table('movielens/users.dat',sep='::',header=None,names=unames)

rnames = ['user_id','movie_id','rating','timestamp']
ratings = pd.read_table('movielens/ratings.dat',sep='::',header=None,names=rnames)

mnames = ['movie_id','title','genres']
movies = pd.read_table('movielens/movies.dat',sep='::',header=None,names=mnames)

利用Python的切片语法,通过查看每个DataFrame的前几行即可验证数据加载工作是否一切顺利。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
users[:5]
Out[49]:
user_id gender age occupation zip
0 1 F 1 10 48067
1 2 M 56 16 70072
2 3 M 25 15 55117
3 4 M 45 7 02460
4 5 M 25 20 55455

ratings[:5]
Out[50]:
user_id movie_id rating timestamp
0 1 1193 5 978300760
1 1 661 3 978302109
2 1 914 3 978301968
3 1 3408 4 978300275
4 1 2355 5 978824291

movies[:5]
Out[54]:
movie_id ... genres
0 1 ... Animation|Children's|Comedy
1 2 ... Adventure|Children's|Fantasy
2 3 ... Comedy|Romance
3 4 ... Comedy|Drama
4 5 ... Comedy

年龄和职业是以编码形式给出

  • Gender is denoted by a “M” for male and “F” for female
  • Age is chosen from the following ranges:
    • 1: “Under 18”
    • 18: “18-24”
    • 25: “25-34”
    • 35: “35-44”
    • 45: “45-49”
    • 50: “50-55”
    • 56: “56+”
  • Occupation is chosen from the following choices:
    • 0: “other” or not specified
    • 1: “academic/educator”
    • 2: “artist”
    • 3: “clerical/admin”
    • 4: “college/grad student”
    • 5: “customer service”
    • 6: “doctor/health care”
    • 7: “executive/managerial”
    • 8: “farmer”
    • 9: “homemaker”
    • 10: “K-12 student”
    • 11: “lawyer”
    • 12: “programmer”
    • 13: “retired”
    • 14: “sales/marketing”
    • 15: “scientist”
    • 16: “self-employed”
    • 17: “technician/engineer”
    • 18: “tradesman/craftsman”
    • 19: “unemployed”
    • 20: “writer”

数据表合并

三表合并,分析分散在三个表中的数据不是一件轻松的事。
先用pandas的merge函数将ratings跟users合并到一起,然后再将movies也合并进去。pandas会根据列名的重叠情况推断出哪些列是合并键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
data = pd.merge(pd.merge(ratings,users),movies)
data.ix[0]

.ix is deprecated. Please use
.loc for label based indexing or
.iloc for positional indexing
data.iloc[0]

data.iloc[0]
Out[58]:
user_id 1
movie_id 1193
rating 5
timestamp 978300760
gender F
age 1
occupation 10
zip 48067
title One Flew Over the Cuckoo's Nest (1975)
genres Drama
Name: 0, dtype: object

统计分析

按性别计算每部电影的平均得分,使用pivot_table方法:

1
2
3
4
5
6
7
8
9
10
11
mean_ratings = data.pivot_table('rating',index='title',columns='gender',aggfunc='mean')

mean_ratings[:5]
Out[74]:
gender F M
title
$1,000,000 Duck (1971) 3.375000 2.761905
'Night Mother (1986) 3.388889 3.352941
'Til There Was You (1997) 2.675676 2.733333
'burbs, The (1989) 2.793478 2.962085
...And Justice for All (1979) 3.828571 3.689024

过滤掉评分数据不够250条的电影。
先对title进行分组,然后利用size()得到一个含有各电影分组大小的Series对象:
Series.index
Series.values

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ratings_by_title = data.groupby('title').size()
ratings_by_title[:10]

Out[75]:
title
$1,000,000 Duck (1971) 37
'Night Mother (1986) 70
'Til There Was You (1997) 52
'burbs, The (1989) 303
...And Justice for All (1979) 199
1-900 (1994) 2
10 Things I Hate About You (1999) 700
101 Dalmatians (1961) 565
101 Dalmatians (1996) 364
12 Angry Men (1957) 616
dtype: int64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
active_titles = ratings_by_title.index[ratings_by_title >=250]
active_titles
Out[83]:
Index([''burbs, The (1989)', '10 Things I Hate About You (1999)',
'101 Dalmatians (1961)', '101 Dalmatians (1996)', '12 Angry Men (1957)',
'13th Warrior, The (1999)', '2 Days in the Valley (1996)',
'20,000 Leagues Under the Sea (1954)', '2001: A Space Odyssey (1968)',
'2010 (1984)',
...
'X-Men (2000)', 'Year of Living Dangerously (1982)',
'Yellow Submarine (1968)', 'You've Got Mail (1998)',
'Young Frankenstein (1974)', 'Young Guns (1988)',
'Young Guns II (1990)', 'Young Sherlock Holmes (1985)',
'Zero Effect (1998)', 'eXistenZ (1999)'],
dtype='object', name='title', length=1216)

该索引中含有评分数据大于250条的电影名称,然后从mean_ratings中选取所需的行:

1
mean_ratings = mean_ratings.loc[active_titles]

为了了解女性观众最喜欢的电影,我们可以对F列降序排列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
top_female_ratings = mean_ratings.sort_values(by='F',ascending=False)
top_female_ratings[:10]
Out[98]:
gender F M
title
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

计算评分分歧

想要找出男性女性观众分歧最大的电影。一个办法是给mean_ratings加上一个用于存放平均得分之差的列,并对其进行排序:

1
mean_ratings['diff'] = mean_ratings['M'] - mean_ratings['F']

按’diff’排序即可得到分歧最大且女性观众更喜欢的电影:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sorted_by_diff = mean_ratings.sort_values(by='diff')
sorted_by_diff[:15]
Out[102]:
gender F M diff
title
Dirty Dancing (1987) 3.790378 2.959596 -0.830782
Jumpin' Jack Flash (1986) 3.254717 2.578358 -0.676359
Grease (1978) 3.975265 3.367041 -0.608224
Little Women (1994) 3.870588 3.321739 -0.548849
Steel Magnolias (1989) 3.901734 3.365957 -0.535777
Anastasia (1997) 3.800000 3.281609 -0.518391
Rocky Horror Picture Show, The (1975) 3.673016 3.160131 -0.512885
Color Purple, The (1985) 4.158192 3.659341 -0.498851
Age of Innocence, The (1993) 3.827068 3.339506 -0.487561
Free Willy (1993) 2.921348 2.438776 -0.482573
French Kiss (1995) 3.535714 3.056962 -0.478752
Little Shop of Horrors, The (1960) 3.650000 3.179688 -0.470312
Guys and Dolls (1955) 4.051724 3.583333 -0.468391
Mary Poppins (1964) 4.197740 3.730594 -0.467147
Patch Adams (1998) 3.473282 3.008746 -0.464536

对排序结果反序并取出前15行,得到的则是男性观众更喜欢的电影:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sorted_by_diff[::-1][:15]
Out[104]:
gender F M diff
title
Good, The Bad and The Ugly, The (1966) 3.494949 4.221300 0.726351
Kentucky Fried Movie, The (1977) 2.878788 3.555147 0.676359
Dumb & Dumber (1994) 2.697987 3.336595 0.638608
Longest Day, The (1962) 3.411765 4.031447 0.619682
Cable Guy, The (1996) 2.250000 2.863787 0.613787
Evil Dead II (Dead By Dawn) (1987) 3.297297 3.909283 0.611985
Hidden, The (1987) 3.137931 3.745098 0.607167
Rocky III (1982) 2.361702 2.943503 0.581801
Caddyshack (1980) 3.396135 3.969737 0.573602
For a Few Dollars More (1965) 3.409091 3.953795 0.544704
Porky's (1981) 2.296875 2.836364 0.539489
Animal House (1978) 3.628906 4.167192 0.538286
Exorcist, The (1973) 3.537634 4.067239 0.529605
Fright Night (1985) 2.973684 3.500000 0.526316
Barb Wire (1996) 1.585366 2.100386 0.515020

如果只是想要找出分歧最大的电影(不考虑性别因素),则可以计算得分数据的方差或标准差:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#根据电影名称分组的得分数据的标准差
rating_std_by_title = data.groupby('title')['rating'].std()
#跟据active_titles进行过滤
rating_std_by_title = rating_std_by_title.iloc[active_titles]
#根据值对Series进行降序排列
rating_std_by_title.sort_values(ascending=False)[:10]
Out[114]:
title
Dumb & Dumber (1994) 1.321333
Blair Witch Project, The (1999) 1.316368
Natural Born Killers (1994) 1.307198
Tank Girl (1995) 1.277695
Rocky Horror Picture Show, The (1975) 1.260177
Eyes Wide Shut (1999) 1.259624
Evita (1996) 1.253631
Billy Madison (1995) 1.249970
Fear and Loathing in Las Vegas (1998) 1.246408
Bicentennial Man (1999) 1.245533
Name: rating, dtype: float64

1880——2010年间全美婴儿姓名

数据整合

由于该数据集按年度被分隔成了多个文件。 所以第一件事情就是要将所有的数据都组装到一个DataFrame里面,并加上一个year字段。使用pandas.concat.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pandas as pd 
import os

os.chdir('/Users/yueqiang/Python Coding/python data analysis data/datasets/')

years = range(1880,2011)
pieces = []
columns = ['name','sex','births']

for year in years:
path = 'babynames/yob%d.txt' % year
frame = pd.read_csv(path,names=columns)
frame['year'] = year
pieces.append(frame)
names = pd.concat(pieces,ignore_index=True)

数据聚合

利用groupby或pivot_table在year和sex级别上对其进行聚合。

1
2
3
4
5
6
7
8
9
10
11
12
total_births = names.pivot_table('births',index='year',columns='sex',aggfunc=sum)
total_births.tail()
Out[122]:
sex F M
year
2006 1896468 2050234
2007 1916888 2069242
2008 1883645 2032310
2009 1827643 1973359
2010 1759010 1898382

total_births.plot(title='Total births by sex and year')

image

插入一个prop列,用于存放指定名字的婴儿数相对于总出生数的比例。prop值为0.02表示每100名婴儿中有2名取了当前这个名字。先按year和sex分组然后再将新列加到各个分组上。

1
2
names = names.groupby(['year','sex']).apply(add_prop)
names.head(10)

在执行这样的分组处理时,一般都应该做一些有效性检查,比如验证所有的分组的prop的总和是否为1。由于这是一个浮点型数据,所以我们应该用np.allclose来检查这个分组总计值是否足够近似于(可能不会精确等于)1:

1
2
np.allclose(names.groupby(['year','sex']).prop.sum(),1)
Out[139]: True

为了便于实现更进一步的分析,需要取出该数据的一个子集;每对sex/year组合的前1000个名字。

1
2
3
4
def get_top1000(group):
return group.sort_index(by='births',ascending = False)[:1000]

top1000 = names.groupby(['year','sex']).apply(get_top1000)

将前1000个名字分为男女两个部分:

1
2
boys = top1000[top1000.sex == 'M']
girls = top1000[top1000.sex == 'F']

生成一张按year和name统计的总出生数透视表:

1
2
3
4
total_births = top1000.pivot_table('births',index = 'year',columns = 'name',aggfunc = sum)
#每年叫做John和Mary的婴儿数
subset = total_births[['John','Harry','Mary','Marilyn']]
subset.plot(subplots=True,figsize=(12,10),grid=False,title="Number of births per year")

image

图2-5 几个男孩和女孩名字随时间变化的使用数量。

这几个名字在美国人民的心目中已经风光不再了。但事实并非如此。

评估命名多样性的成长

图2-5所反映的降低情况可能意味着父母愿意给小孩起常见的名字越来越少。这个假设可以从数据中得到验证。一个办法是计算最流行的1000个名字所占的比例,我按year和sex进行聚合并绘图:

1
2
table = top1000.pivot_table('prop',index='year',columns='sex',aggfunc=sum)
table.plot(title='Sum of table1000.prop by year and sex',yticks=np.linspace(0,1.2,13),xticks=range(1880,2020,10))

image

图2-6:分别统计的前1000个名字在总出生人数中的比例

从图2-6可以看出,名字的多样性确实出现了增长(前1000项的比例降低)。
另一种方式是计算占总出生人数前50%的不同名字的数量,这个数字不太好计算。只考虑2010年男孩的名字:

1
df = boys[boys.year == 2010]

在对prop降序排列之后,我们想知道前面多少个名字的人数加起来才够50%。虽然编写一个for循环确实也能达到目的,但Numpy有一种更聪明的矢量方式。先计算prop的累计和cumsum,然后再通过searchsorted方法找出0.5应该被插入在哪个位置才能保证不破坏顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
prop_cumsum = df.sort_values(by='prop',ascending=False).prop.cumsum()
prop_cumsum[:10]
Out[150]:
year sex
2010 M 1676644 0.011523
1676645 0.020934
1676646 0.029959
1676647 0.038930
1676648 0.047817
1676649 0.056579
1676650 0.065155
1676651 0.073414
1676652 0.081528
1676653 0.089621
Name: prop, dtype: float64
prop_cumsum.searchsorted(0.5)
Out[151]: array([116])

由于索引是从0开始的,因此我们要给这个结果加1,即最终结果为117。拿1900年的数据来做个比较,这个数字要小得多:

1
2
3
4
df = boys[boys.year == 1900]
in1900 = df.sort_values(by='prop',ascending=False).prop.cumsum()
in1900.searchsorted(0.5) + 1
Out[159]: array([25])

现在就可以对所有的year/sex组合执行这个计算了。按这两个字段进行groupby处理,然后用一个函数计算各分组的这个值:

1
2
3
4
5
6
def get_quantile_count(group,q=0.5):
group = group.sort_values(by='prop',ascending=False)
return group.prop.cumsum().searchsorted(q) + 1

diversity = top1000.groupby(['year','sex']).apply(get_quantile_count)
diversity = diversity.unstack('sex')

diversity这个DataFrame拥有两个时间序列(每个性别各一个,按年度索引)。
通过IPython,你可以查看其内容,还可以像之前那样绘制图表(如图2-7所示)。

1
2
3
4
5
6
7
8
9
diversity.head()
Out[172]:
sex F M
year
1880 38 14
1881 38 14
1882 38 15
1883 39 15
1884 39 16
1
diversity.plot(title='Number of popular names in top 50%')

image

图2-7:按年度统计的密度表

从图可以看出,女孩名字的多样性总是比男孩的高,而且还在变得越来越高。

“最后一个字母”的变革

近百年来,男孩的名字在最后一个字母上的分布发生了显著的变化。为了了解具体的情况,我首先将全部出生数据在年度、性别以及末字母上进行了聚合:

1
2
3
4
#从name列取出最后一个字母
get_last_letter = lambda x:x[-1]
last_letter = names.name.map(get_last_letter)
last_letter.name = 'last_letter'
1
2
3
4
5
6
7
8
9
10
11
subtable = table.reindex(columns=[1910,1960,2010],level='year')
subtable.head()
Out[180]:
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

接下来需要按总出生数对表进行规范化处理,以便计算出各性别各末字母占总出生人数的比例:

1
2
3
4
5
6
7
8
9
10
subtable.sum()
Out[188]:
sex year
F 1910 396416.0
1960 2022062.0
2010 1759010.0
M 1910 194198.0
1960 2132588.0
2010 1898382.0
dtype: float64
1
letter_prop = subtable/subtable.sum().astype(float)

有了这个字母比例数据之后,就可以生成一张各年度各性别的条形图了,如图2-8所示:

1
2
3
4
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[0],title='Female',legend=False)

image

图2-8:男孩女孩名字中各个末字母的比例

从图2-8中可以看出,从20世纪60年代开始,以字母‘n’结尾的男孩名字出现了显著的增长。

回到之前创建的那个完整表,按年度和性别对其进行规范化处理,并在男孩名字中选取几个名字,最后进行转置以便将各个列做成一个时间序列:

letter_prop = table/table.sum().astype(float)
dny_ts = letter_prop.loc[[‘d’,’n’,’y’],’M’].T
dny_ts.head()

有了这个时间序列的DataFrame之后,就可以通过其plot方法绘制出一张趋势图了(如图2-9所示):

1
dny_ts.plot()

image

图2-9:各年出生的男孩中名字以d/n/y结尾的人数比例

变成女孩名字的男孩名字(以及相反的情况)

另一个有趣的趋势是,早年流行于男孩的名字今年来“变性了”,例如Lesley或Leslie。回到top1000数据集,找出其中以”lesl”开头的一组名字:

1
2
3
4
5
all_names = top1000.name.unique()
mask = np.array(['lesl'] in x.lower() for x in all_names])
lesley_like = all_name[mask]
lesley_like
Out[207]: array(['Leslie', 'Lesley', 'Leslee', 'Lesli', 'Lesly'], dtype=object)

然后利用这个结果过滤其他的名字,并按名字分组计算出生数以查看相对频率:

1
2
3
4
5
6
7
8
9
10
filtered = top1000[top1000.name.isin(lesley_like)]
filtered.groupby('name').births.sum()
Out[208]:
name
Leslee 1082
Lesley 35022
Lesli 929
Leslie 370429
Lesly 10067
Name: births, dtype: int64

接下来,我们按姓名和年度进行聚合,并按年度进行规范化处理:

1
2
3
4
5
6
7
8
9
10
11
table = filterd.pivot_table('births',index='year',columns='sex',aggfunc='sum')
table = table.div(table.sum(1),axis=0)
table.tail()
Out[213]:
sex F M
year
2006 1.0 NaN
2007 1.0 NaN
2008 1.0 NaN
2009 1.0 NaN
2010 1.0 NaN

绘制一张分性别的年度曲线图

1
table.plot(style={'M':'k-','F':'k--'})

image

图2-10:各年度使用“Lesley型”名字的男女比例

Directory
  1. 1. ch02
    1. 1.1. 来自bit.ly的1.usa.gov数据
      1. 1.1.1. 列表推导式,后加 if 判断字段是否存在
      2. 1.1.2. 对时区计数——pandas库
      3. 1.1.3. 前十位的时区及其计数
      4. 1.1.4. 使用collections.Counter类计数更加方便
      5. 1.1.5. 用pandas对时区进行计数
      6. 1.1.6. matplotlib数据可视化
      7. 1.1.7. 对分组结果进行计数,并利用unstack对计数结果进行重塑
    2. 1.2. MovieLens 1M 数据集
      1. 1.2.1. 数据导入
      2. 1.2.2. 数据表合并
      3. 1.2.3. 统计分析
      4. 1.2.4. 计算评分分歧
    3. 1.3. 1880——2010年间全美婴儿姓名
      1. 1.3.1. 数据整合
      2. 1.3.2. 数据聚合
      3. 1.3.3. 评估命名多样性的成长
      4. 1.3.4. “最后一个字母”的变革
      5. 1.3.5. 变成女孩名字的男孩名字(以及相反的情况)