说明:本文内容翻译、节选自外文From Pandas-wan to Pandas-master[1],原作者Rudolf Höhn小哥,实验数据来自kaggle [2]的各国自杀率预测竞赛,都需要访问外国网站,相关文件已上传到本人的github中[3][4]。
本文主要包含如下部分:
1.数据探索2.内存优化3.索引4.方法串联(method chaining)
import pandas as pd
import numpy as np
import os
data_path = 'path/to/folder/' # 注意修改路径
df = (pd.read_csv(filepath_or_buffer=os.path.join(data_path, 'master.csv'))
.rename(columns={'suicides/100k pop' : 'suicides_per_100k',
' gdp_for_year ($) ' : 'gdp_year',
'gdp_per_capita ($)' : 'gdp_capita',
'country-year' : 'country_year'})
.assign(gdp_year=lambda _df: _df['gdp_year'].str.replace(',','').astype(np.int64))
)
df.describe(include='all')
df.dtypes
country object
year int64
sex object
age object
suicides_no int64
population int64
suicides_per_100k float64
country_year object
HDI for year float64
gdp_year int64
gdp_capita int64
generation object
dtype: object
在处理数据之前,一个重要的步骤是理解数据并为各列数据选择合适的数据类型,这里有两种方法可以显著地降低你的内存消耗。
import pandas as pd
def mem_usage(df: pd.DataFrame) -> str:
"""This method styles the memory usage of a DataFrame to be readable as MB.
Parameters
----------
df: pd.DataFrame
Data frame to measure.
Returns
-------
str
Complete memory usage as a string formatted for MB.
"""
return f'{df.memory_usage(deep=True).sum() / 1024 ** 2 : 3.2f} MB'
def convert_df(df: pd.DataFrame, deep_copy: bool = True) -> pd.DataFrame:
"""Automatically converts columns that are worth stored as
``categorical`` dtype.
Parameters
----------
df: pd.DataFrame
Data frame to convert.
deep_copy: bool
Whether or not to perform a deep copy of the original data frame.
Returns
-------
pd.DataFrame
Optimized copy of the input data frame.
"""
return df.copy(deep=deep_copy).astype({
col: 'category' for col in df.columns
if df[col].nunique() / df[col].shape[0] < 0.5})
pandas 提供了 memory_usage()方法来分析数据的内存消耗,在代码中,deep = True 确保真正使用了系统内存。理解列的类型非常重要,这可以节省你90%以上的内存。比如对与price这一列来讲,float64浮点类型可能会产生不必要的消耗,所以要尽量使用int32型。
回到我们定义的convert_df()方法上来,如果某一列百分之50以上的值都是独一无二的(unique),它可以自动地把列的类型转换为类别变量。
让我们看看数据都发生了什么神奇变化吧!
>>> mem_usage(df)
10.28 MB
>>> mem_usage(df.set_index(['country', 'year', 'sex', 'age']))
5.00 MB
>>> mem_usage(convert_df(df))
1.40 MB
>>> mem_usage(convert_df(df.set_index(['country', 'year', 'sex', 'age'])))
1.40 MB
通过变换,datafram数据的内存消耗只有原来的十分之一了!!!
在pandas中,我们有两种方式获得数据,一种是通过索引(indexing),另外一种是通过查询(query),在大多数情况下,通过索引(或者多重索引)效果更佳,让我们看一下例子吧!
>>> %%time
>>> df.query('country == "Albania" and year == 1987 and sex == "male" and age == "25-34 years"') # 查询
CPU times: user 7.27 ms, sys: 751 µs, total: 8.02 ms
# =========下面使用多重索引=========
%%time
mi_df = df.set_index(['country', 'year', 'sex', 'age']) # 创建多重索引的时间
CPU times: user 10.8 ms, sys: 2.2 ms, total: 13 ms
>>> %%time
>>> mi_df.loc['Albania', 1987, 'male', '25-34 years'] #
CPU times: user 459 µs, sys: 1 µs, total: 460 µs
#===================
query方式总共消耗了7.27ms , 索引方式总的消耗时间为(创建多重索引的时间10秒)+(查询的时间459us)
所以,如果你只使用一次数据(当然这种情况很少见),请使用query查询方式, 否则使用索引方式,因为一旦我们有了多重索引,通过索引获取数据相当高效。
‘’方法串联‘’ 指把 一系列的多个方法(method)串联起来,最后返回dataframe,这样可以避免中间变量的产生,从而节省内存。
import numpy as np
import pandas as pd
df = pd.DataFrame({'a_column': [1, -999, -999],
'powerless_column': [2, 3, 4],
'int_column': [1, 1, -1]})
df['a_column'] = df['a_column'].replace(-999, np.nan)
df['power_column'] = df['powerless_column'] ** 2
df['real_column'] = df['int_column'].astype(np.float64)
df = df.apply(lambda _df: _df.replace(4, np.nan))
df = df.dropna(how='all')
df = (pd.DataFrame({'a_column': [1, -999, -999],
'powerless_column': [2, 3, 4],
'int_column': [1, 1, -1]})
.assign(a_column=lambda _df: _df['a_column'].replace(-999, np.nan),
power_column=lambda _df: _df['powerless_column'] ** 2,
real_column=lambda _df: _df['int_column'].astype(np.float64))
.apply(lambda _df: _df.replace(4, np.nan))
.dropna(how='all')
)
对我们的自杀数据进行处理:
(df
.groupby('age')
.agg({'generation':'unique'})
.rename(columns={'generation':'unique_generation'})
# Recommended from v0.25
# .agg(unique_generation=('generation', 'unique'))
)
age | unique_generation |
---|---|
15-24 years | [Generation X, Millenials] |
25-34 years | [Boomers, Generation X, Millenials] |
35-54 years | [Silent, Boomers, Generation X] |
5-14 years | [Generation X, Millenials, Generation Z] |
55-74 years | [G.I. Generation, Silent, Boomers] |
75+ years | [G.I. Generation, Silent] |
上述的代码先是对df进行年龄分组,返回一个dataFrameGroupBy的类型数据,之后再个各个组进行聚合操作(agg),得到每组独一无二的值。
该方法也可以接受任意函数(functions),在0.25版本的pandas中,新增了新的使用agg的方式:
#使用sort_values函数和head 函数 排序并得到前10名
(df
.groupby(['country', 'year'])
.agg({'suicides_per_100k': 'sum'})
.rename(columns={'suicides_per_100k':'suicides_sum'})
# Recommended from v0.25
# .agg(suicides_sum=('suicides_per_100k', 'sum'))
.sort_values('suicides_sum', ascending=False)
.head(10)
country | year | suices_sum |
---|---|---|
Lithuania | 1995 | 639.30 |
1996 | 595.61 | 580 |
Hungary | 1991 | 575.00 |
Lithuania | 2000 | 571.80 |
Hungary | 1992 | 570.26 |
Lithuania | 2001 | 568.98 |
Russian Federation | 1994 | 567.64 |
Lithuania | 1998 | 566.36 |
1997 | 565.44 | 577 |
1999 | 561.53 | 899 |
#直接使用nlargest 函数得到新列suicides_sum的前10名
(df
.groupby(['country', 'year'])
.agg({'suicides_per_100k': 'sum'})
.rename(columns={'suicides_per_100k':'suicides_sum'})
# Recommended from v0.25
# .agg(suicides_sum=('suicides_per_100k', 'sum'))
.nlargest(10, columns='suicides_sum')
)
country | year | suicides_sum |
---|---|---|
Lithuania | 1995 | 639.30 |
1996 | 595.61 | 435 |
Hungary | 1991 | 575.00 |
Lithuania | 2000 | 571.80 |
Hungary | 1992 | 570.26 |
Lithuania | 2001 | 568.98 |
Russian Federation | 1994 | 567.64 |
Lithuania | 1998 | 566.36 |
1997 | 565.44 | 567 |
1999 | 561.53 | 434 |
两种方法,使用nlargest更加简洁些。
另外一个有趣的方法是unstack,其允许反转坐标轴。
mi_df.loc[('Switzerland', 2000)]
可以看到,上面数据的行索引是性别、年龄,将性别展开(unstack)后,选择自杀数 和人口数这两列,得到如下,以前性别作为行索引,现在性别变成了列
(mi_df
.loc[('Switzerland', 2000)]
.unstack('sex')
[['suicides_no', 'population']]
)
如果我们不取自杀数和人口数这两列,只unstack('sex'):
另外一种方法是使用pipe进行串联操作,一个简单有效的例子是查询数据的不同信息:
def log_head(df, head_count=10):
print(df.head(head_count))
return df
def log_columns(df):
print(df.columns)
return df
def log_shape(df):
print(f'shape = {df.shape}')
return df
(df
.pipe(log_shape)
.query('sex == "female"')
.groupby(['year', 'country'])
.agg({'suicides_per_100k':'sum'})
.pipe(log_shape)
.rename(columns={'suicides_per_100k':'sum_suicides_per_100k_female'})
# Recommended from v0.25
# .agg(sum_suicides_per_100k_female=('suicides_per_100k', 'sum'))
.nlargest(n=10, columns=['sum_suicides_per_100k_female'])
)
shape = (27820, 12)
shape = (2321, 1)
year | country | sum_suicedes_per_100k_femal |
---|---|---|
2009 | Republic of Korea | 170.89 |
1989 | Singapore | 163.16 |
1986 | Singapore | 161.67 |
2010 | Republic of Korea | 158.52 |
2007 | Republic of Korea | 149.60 |
2011 | Republic of Korea | 147.84 |
1991 | Hungary | 147.35 |
2008 | Republic of Korea | 147.04 |
2000 | Aruba | 146.22 |
2005 | Republic of Korea | 145.35 |
[1]
: https://medium.com/unit8-machine-learning-publication/from-pandas-wan-to-pandas-master-4860cf0ce442 [2]
: https://www.kaggle.com/russellyates88/suicide-rates-overview-1985-to-2016/downloads/suicide-rates-overview-1985-to-2016.zip/1 [3]
: https://github.com/deepwindlee/MySQL-with-Python-DATA-MINING/blob/master/from_pandas-wan_to_pandas-master.ipynb [4]
: https://github.com/deepwindlee/MySQL-with-Python-DATA-MINING/blob/master/suicide.csv