TL;DR
group_cols = ['City', 'Card', 'Colateral']
debt_col = '% Debt Paid'
# (1) Bin the data that is in non-zero-width intervals
bins = pd.IntervalIndex.from_breaks((0, 0.25, 0.5, 0.75, 1, np.inf),
closed='right')
ser_pt1 = df.groupby(group_cols, sort=False)[debt_col]\
.value_counts(bins=bins, sort=False, normalize=True)
# (2) Get the data from zero width intervals (0% and 100%)
ser_pt2 = df[df[debt_col].isin((0, 1))]\
.groupby(group_cols)[debt_col].value_counts()
# Take also "zero counts" and normalize
ser_pt2 = ser_pt2.reindex(
pd.MultiIndex.from_product(ser_pt2.index.levels,
names=ser_pt2.index.names),
fill_value=0) / df.groupby(group_cols)[debt_col].count()
# (3) Combine the results
ser_out = pd.concat([ser_pt1, ser_pt2])
这是一个简单粗暴的答案。下面是一个可复制粘贴的完整答案,它还按照问题的要求制作索引名称和排序。
1。总结
问题变得更难解决,因为您想要的箱子是相交的。也就是说,您希望拥有]75, 100] 和[100, 100] 的bin,它们都应该包括% Debt Paid 是1.0 的情况。我将分别处理两种情况
(1) 值 ]0, 25]%、]25, 50]%、...、]100%, np.inf]%
(2) 0% 和 100%
2。解决方案说明
2.1 分箱部分
2.2 其余部分(0% 和 100%)
2.3 重命名
- 我们的新索引(级别 3,称为“bin”)现已准备就绪,但要获得与 OP 问题中相同的输出,我们需要重命名索引标签。只需遍历值并使用“查找字典”查找新名称即可完成此操作
- 默认情况下,索引中标签的顺序取自数字/字母顺序,但这不是我们想要的。要在排序后强制索引顺序,我们必须使用pd.Categorical 作为索引。排序顺序在
categories 参数中给出。我们依赖于在 python 3.6+ 字典中保留排序的事实。
- 出于某种原因,
ser_out.sort_index() 即使使用分类索引也无法正常工作。我认为这可能是熊猫中的一个错误。因此,将结果 Series ser_out 强制转换为 DataFrame df_out,并使用 dataframe 进行排序。
- 最后,生成的数据帧使用
set_index 制成 MultiIndex。
代码
零宽度的 bin 会导致 value_counts 产生非常奇怪的结果。也许这是熊猫的错误。因此,我们将问题分为两步(1)统计非零宽度bin中的数据(2)统计零宽度bin(“0%”和“100%”)中的数据
import pandas as pd
import numpy as np
d = {'City': ['Tokyo','Tokyo','Lisbon','Tokyo','Tokyo','Lisbon','Lisbon','Lisbon','Tokyo','Lisbon','Tokyo','Tokyo','Tokyo','Lisbon','Tokyo','Tokyo','Lisbon','Lisbon','Lisbon','Tokyo','Lisbon','Tokyo'],
'Card': ['Visa','Visa','Master Card','Master Card','Visa','Master Card','Visa','Visa','Master Card','Visa','Master Card','Visa','Visa','Master Card','Master Card','Visa','Master Card','Visa','Visa','Master Card','Visa','Master Card'],
'Colateral':['Yes','No','Yes','No','No','No','No','Yes','Yes','No','Yes','Yes','No','Yes','No','No','No','Yes','Yes','No','No','No'],
'Client Number':[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22],
'% Debt Paid':[0.8,0.1,0.5,0.30,0,0.2,0.4,1,0.60,1,0.5,0.2,0,0.3,0,0,0.2,0,0.1,0.70,0.5,0.1]}
df = pd.DataFrame(data=d)
def _get_binned_part(df, group_cols, debt_col):
bins = pd.IntervalIndex.from_breaks((0, 0.25, 0.5, 0.75, 1, np.inf),
closed='right')
gp = df[group_cols + [debt_col]].groupby(group_cols, sort=False)
ser_pt1 = gp[debt_col].value_counts(bins=bins, sort=False, normalize=True)
ser_pt1.index.set_names('bin', level=3, inplace=True)
return ser_pt1
def _get_non_binned_part(df, group_cols, debt_col):
# Count 0% and 100% occurences
ser_pt2 = df[df[debt_col].isin((0, 1))]\
.groupby(group_cols)[debt_col].value_counts()
# include zero counts
ser_pt2 = ser_pt2.reindex(pd.MultiIndex.from_product(
ser_pt2.index.levels, names=ser_pt2.index.names),
fill_value=0)
ser_pt2.index.set_names('bin', level=3, inplace=True)
# ser_counts has the counts for normalization.
ser_counts = df.groupby(group_cols)[debt_col].count()
ser_pt2 = ser_pt2 / ser_counts
return ser_pt2
def _rename_bins(ser_out, group_cols, debt_col):
bin_names = []
bin_name_dict = {
'0.0': '0%',
'(0.0, 0.25]': ']0, 25]%',
'(0.25, 0.5]': ']25, 50]%',
'(0.5, 0.75]': ']50, 75]%',
'(0.75, 1.0]': ']75, 100]%',
'1.0': '100%',
'(1.0, inf]': '>100%',
}
bin_order = list(bin_name_dict.values())
for val in ser_out.index.levels[3].values:
bin_names.append(bin_name_dict.get(val.__str__(), val.__str__()))
bin_categories = pd.Categorical(bin_names,
categories=bin_order,
ordered=True)
ser_out.index.set_levels(bin_categories, level=3, inplace=True)
# For some reason, .sort_index() does not sort correcly
# -> Make it a dataframe and sort there.
df_out = ser_out.reset_index()
df_out['bin'] = pd.Categorical(df_out['bin'].values,
bin_order,
ordered=True)
df_out = df_out.sort_values(group_cols + ['bin']).set_index(group_cols +
['bin'])
df_out.rename(columns={debt_col: 'in_bin'}, inplace=True)
df_out['in_bin'] = (df_out['in_bin'] * 100).round(2)
return df_out
def get_results(df):
group_cols = ['City', 'Card', 'Colateral']
debt_col = '% Debt Paid'
ser_pt1 = _get_binned_part(df, group_cols, debt_col)
ser_pt2 = _get_non_binned_part(df, group_cols, debt_col)
ser_out = pd.concat([ser_pt1, ser_pt2])
df_out = _rename_bins(ser_out, group_cols, debt_col)
return df_out
df_out = get_results(df)
示例输出
In [1]: df_out
Out[1]:
in_bin
City Card Colateral bin
Lisbon Master Card No 0% 0.00
]0, 25]% 100.00
]25, 50]% 0.00
]50, 75]% 0.00
]75, 100]% 0.00
100% 0.00
>100% 0.00
Yes 0% 0.00
]0, 25]% 0.00
]25, 50]% 100.00
]50, 75]% 0.00
]75, 100]% 0.00
100% 0.00
>100% 0.00
Visa No 0% 0.00
]0, 25]% 0.00
]25, 50]% 66.67
]50, 75]% 0.00
]75, 100]% 33.33
100% 33.33
>100% 0.00
Yes 0% 33.33
]0, 25]% 33.33
]25, 50]% 0.00
]50, 75]% 0.00
]75, 100]% 33.33
100% 33.33
>100% 0.00
Tokyo Master Card No 0% 25.00
]0, 25]% 25.00
]25, 50]% 25.00
]50, 75]% 25.00
]75, 100]% 0.00
100% 0.00
>100% 0.00
Yes 0% 0.00
]0, 25]% 0.00
]25, 50]% 50.00
]50, 75]% 50.00
]75, 100]% 0.00
100% 0.00
>100% 0.00
Visa No 0% 75.00
]0, 25]% 25.00
]25, 50]% 0.00
]50, 75]% 0.00
]75, 100]% 0.00
100% 0.00
>100% 0.00
Yes 0% 0.00
]0, 25]% 50.00
]25, 50]% 0.00
]50, 75]% 0.00
]75, 100]% 50.00
100% 0.00
>100% 0.00
附录
所需的示例输出:“里斯本,签证,否”
有了这个组合
In [1]: df.loc[ (df['City'] == 'Lisbon') & (df['Card'] == 'Visa') & (df['Colateral'] == 'No')]
Out[1]:
City Card Colateral Client Number % Debt Paid
6 Lisbon Visa No 7 0.4
9 Lisbon Visa No 10 1.0
20 Lisbon Visa No 21 0.5
输出数据表应该有
0% 0%
]0, 25]% 0%
]25, 50]% 66.7%
]50, 75]% 0%
]75, 100]% 33.3%
100% 33.3%
>100% 0%
请注意,一对相交的 bin 对(]75, 100] 和 [100, 100])会导致输出列的总和有时大于 100%。