我注意到Stack Overflow的用户数量及其声誉遵循有趣的分布。我创建了一个 pandas DF ,看看我是否可以创建一个参数拟合:
import pandas as pd
import numpy as np
soDF = pd.read_excel('scores.xls')
print soDF
返回此内容:
total_rep users
0 1 4364226
1 200 269110
2 500 158824
3 1000 90368
4 2000 48609
5 3000 32604
6 5000 18921
7 10000 8618
8 25000 2802
9 50000 1000
10 100000 334
如果我绘制图表,我会得到以下图表:
分布似乎遵循 Power Law 。因此,为了更好地可视化,我添加了以下内容:
soDF['log_total_rep'] = soDF['total_rep'].apply(np.log10)
soDF['log_users'] = soDF['users'].apply(np.log10)
soDF.plot(x='log_total_rep', y='log_users')
是否有一种简单的方法可以使用pandas来找到最适合这些数据的方法?虽然拟合看起来是线性的,但也许多项式拟合更好,因为现在我处理的是对数刻度。
答案 0 :(得分:10)
python
,pandas
和scipy
,哦,我的!科学python生态系统有几个免费的库。按设计,没有一个图书馆可以做任何事情pandas
提供了处理类似表格的数据和时间序列的工具。但是,它故意不包含您正在寻找的功能类型。
对于拟合统计分布,您通常使用其他包,例如scipy.stats
。
然而,在这种情况下,我们没有" raw"数据(即一长串的声誉分数)。相反,我们有类似于直方图的东西。因此,我们需要使用比scipy.stats.powerlaw.fit
更低的水平。
目前,让我们完全放弃pandas
。在这里使用它没有任何优势,我们无论如何都会迅速将数据帧转换为其他数据结构。 pandas
很棒,对于这种情况来说只是过度杀伤。
作为重现情节的快速独立示例:
import matplotlib.pyplot as plt
total_rep = [1, 200, 500, 1000, 2000, 3000, 5000, 10000,
25000, 50000, 100000]
num_users = [4364226, 269110, 158824, 90368, 48609, 32604,
18921, 8618, 2802, 1000, 334]
fig, ax = plt.subplots()
ax.loglog(total_rep, num_users)
ax.set(xlabel='Total Reputation', ylabel='Number of Users',
title='Log-Log Plot of Stackoverflow Reputation')
plt.show()
接下来,我们需要知道我们正在使用什么。我们绘制的内容类似于直方图,因为它是给定信誉级别的用户数的原始计数。但请注意小" +"在每个bin旁边的信誉表。这意味着,例如,2082个用户的信誉得分为25000 或更高。
我们的数据基本上是互补累积分布函数(CCDF)的估计,与直方图是概率分布函数(PDF)的估计相同。我们只需要通过我们样本中的用户总数对其进行标准化,以获得CCDF的估算值。在这种情况下,我们可以简单地除以num_users
的第一个元素。信誉永远不会小于1,因此x轴上的1对应于定义1的概率。 (在其他情况下,我们需要估计这个数字。)例如:
import numpy as np
import matplotlib.pyplot as plt
total_rep = np.array([1, 200, 500, 1000, 2000, 3000, 5000, 10000,
25000, 50000, 100000])
num_users = np.array([4364226, 269110, 158824, 90368, 48609, 32604, 18921,
8618, 2802, 1000, 334])
ccdf = num_users.astype(float) / num_users.max()
fig, ax = plt.subplots()
ax.loglog(total_rep, ccdf, color='lightblue', lw=2, marker='o',
clip_on=False, zorder=10)
ax.set(xlabel='Reputation', title='CCDF of Stackoverflow Reputation',
ylabel='Probability that Reputation is Greater than X')
plt.show()
您可能想知道我们为什么要将事情转换为"标准化"版。最简单的答案是,它更有用。它允许我们说出与我们的样本量没有直接关系的东西。明天,Stackoverflow用户的总数(以及每个信誉级别的数字)将有所不同。但是,任何给定用户具有特定声誉的总概率都不会发生显着变化。如果我们想要在网站达到500万注册用户时预测John Skeet的声誉(最高代表用户),那么使用概率而不是原始计数要容易得多。
接下来,让我们为CCDF拟合幂律分布。再次,如果我们有" raw"数据以长信誉分数列表的形式出现,最好使用统计软件包来处理这个问题。特别是scipy.stats.powerlaw.fit
。
但是,我们没有原始数据。幂律分布的CCDF采用ccdf = x**(-a + 1)
的形式。因此,我们在日志空间中输入一行,我们可以从a
获取分布的a = 1 - slope
参数。
目前,让我们使用np.polyfit
来适应这条线。我们需要自己在日志空间来回处理转换:
import numpy as np
import matplotlib.pyplot as plt
total_rep = np.array([1, 200, 500, 1000, 2000, 3000, 5000, 10000,
25000, 50000, 100000])
num_users = np.array([4364226, 269110, 158824, 90368, 48609, 32604, 18921,
8618, 2802, 1000, 334])
ccdf = num_users.astype(float) / num_users.max()
# Fit a line in log-space
logx = np.log(total_rep)
logy = np.log(ccdf)
params = np.polyfit(logx, logy, 1)
est = np.exp(np.polyval(params, logx))
fig, ax = plt.subplots()
ax.loglog(total_rep, ccdf, color='lightblue', ls='', marker='o',
clip_on=False, zorder=10, label='Observations')
ax.plot(total_rep, est, color='salmon', label='Fit', ls='--')
ax.set(xlabel='Reputation', title='CCDF of Stackoverflow Reputation',
ylabel='Probability that Reputation is Greater than X')
plt.show()
这种合适的问题直接存在。我们的估计表明,大于1 的概率,用户的声誉为1.这是不可能的。
问题在于我们让polyfit
为我们的行选择最合适的y截距。如果我们在上面的代码中查看params
,那么它就是第二个数字:
In [11]: params
Out[11]: array([-0.81938338, 1.15955974])
根据定义,y轴截距应为1.相反,最佳拟合截距约为1.16
。我们需要修正这个数字,并且只允许斜率在线性拟合中变化。
首先,请注意log(1) --> 0
。因此,我们实际上想要强制日志空间中的y轴截距为0而不是1。
最简单的方法是使用np.linalg.lstsq
解决问题,而不是np.polyfit
。无论如何,你可以做类似的事情:
import numpy as np
import matplotlib.pyplot as plt
total_rep = np.array([1, 200, 500, 1000, 2000, 3000, 5000, 10000,
25000, 50000, 100000])
num_users = np.array([4364226, 269110, 158824, 90368, 48609, 32604, 18921,
8618, 2802, 1000, 334])
ccdf = num_users.astype(float) / num_users.max()
# Fit a line with a y-intercept of 1 in log-space
logx = np.log(total_rep)
logy = np.log(ccdf)
slope, _, _, _ = np.linalg.lstsq(logx[:,np.newaxis], logy)
params = [slope, 0]
est = np.exp(np.polyval(params, logx))
fig, ax = plt.subplots()
ax.loglog(total_rep, ccdf, color='lightblue', ls='', marker='o',
clip_on=False, zorder=10, label='Observations')
ax.plot(total_rep, est, color='salmon', label='Fit', ls='--')
ax.set(xlabel='Reputation', title='CCDF of Stackoverflow Reputation',
ylabel='Probability that Reputation is Greater than X')
plt.show()
嗯......现在我们遇到了一个新问题。我们的新产品线并不适合我们的数据。这是幂律分布的常见问题。
在现实生活中,观察到的分布几乎从未完全遵循幂律。然而,他们的长尾巴#34;经常这样做。您可以在此数据集中清楚地看到这一点。如果我们要排除前两个数据点(低信誉/高概率),我们会得到一条非常不同的线,它将更好地适应剩余的数据。
事实上,只有分布的尾部遵循幂律,这解释了为什么当我们修正y截距时,我们无法很好地拟合数据。
对于在概率为1附近发生的事情,有许多不同的修正幂律模型,但它们都遵循一些截止值右边的幂律。根据我们观察到的数据,看起来我们可以拟合两条线:一条在信誉的右边,约为1000,一条在左边。
考虑到这一点,让我们忘记事物的左手边,并专注于长尾"在右边。我们会使用np.polyfit
但排除最合适的最左边三个点。
import numpy as np
import matplotlib.pyplot as plt
total_rep = np.array([1, 200, 500, 1000, 2000, 3000, 5000, 10000,
25000, 50000, 100000])
num_users = np.array([4364226, 269110, 158824, 90368, 48609, 32604, 18921,
8618, 2802, 1000, 334])
ccdf = num_users.astype(float) / num_users.max()
# Fit a line in log-space, excluding reputation <= 1000
logx = np.log(total_rep[total_rep > 1000])
logy = np.log(ccdf[total_rep > 1000])
params = np.polyfit(logx, logy, 1)
est = np.exp(np.polyval(params, logx))
fig, ax = plt.subplots()
ax.loglog(total_rep, ccdf, color='lightblue', ls='', marker='o',
clip_on=False, zorder=10, label='Observations')
ax.plot(total_rep[total_rep > 1000], est, color='salmon', label='Fit', ls='--')
ax.set(xlabel='Reputation', title='CCDF of Stackoverflow Reputation',
ylabel='Probability that Reputation is Greater than X')
plt.show()
在这种情况下,我们有一些额外的数据。让我们看看每个不同的契合度如何预测前5位用户的声誉:
import numpy as np
import matplotlib.pyplot as plt
total_rep = np.array([1, 200, 500, 1000, 2000, 3000, 5000, 10000,
25000, 50000, 100000])
num_users = np.array([4364226, 269110, 158824, 90368, 48609, 32604, 18921,
8618, 2802, 1000, 334])
top_5_rep = [832131, 632105, 618926, 596889, 576697]
top_5_ccdf = np.array([1, 2, 3, 4, 5], dtype=float) / num_users.max()
ccdf = num_users.astype(float) / num_users.max()
# Previous fits
naive_params = [-0.81938338, 1.15955974]
fixed_intercept_params = [-0.68845134, 0]
long_tail_params = [-1.26172528, 5.24883471]
fits = [naive_params, fixed_intercept_params, long_tail_params]
fit_names = ['Naive Fit', 'Fixed Intercept Fit', 'Long Tail Fit']
fig, ax = plt.subplots()
ax.loglog(total_rep, ccdf, color='lightblue', ls='', marker='o',
clip_on=False, zorder=10, label='Observations')
# Plot reputation of top 5 users
ax.loglog(top_5_rep, top_5_ccdf, ls='', marker='o', color='darkred',
zorder=10, label='Top 5 Users')
# Plot different fits
for params, name in zip(fits, fit_names):
x = [1, 1e7]
est = np.exp(np.polyval(params, np.log(x)))
ax.loglog(x, est, label=name, ls='--')
ax.set(xlabel='Reputation', title='CCDF of Stackoverflow Reputation',
ylabel='Probability that Reputation is Greater than X',
ylim=[1e-7, 1])
ax.legend()
plt.show()
哇!他们都做得非常糟糕!首先,这是在拟合分布而不仅仅是分箱数据时使用完整系列的一个很好的理由。然而,问题的根源在于,在这种情况下,幂律分布并不是非常合适。乍一看,看起来像指数分布可能更合适,但让我们留待以后。
作为一个例子,不同的幂律适合过度预测低概率观察(即具有最高代表的用户),让我们预测Jon Skeet在每个模型中的声誉:
import numpy as np
# Jon Skeet's actual reputation
skeet_prob = 1.0 / 4364226
true_rep = 832131
# Previous fits
naive_params = [-0.81938338, 1.15955974]
fixed_intercept_params = [-0.68845134, 0]
long_tail_params = [-1.26172528, 5.24883471]
fits = [naive_params, fixed_intercept_params, long_tail_params]
fit_names = ['Naive Fit', 'Fixed Intercept Fit', 'Long Tail Fit']
for params, name in zip(fits, fit_names):
inv_params = [1 / params[0], -params[1]/params[0]]
est = np.exp(np.polyval(inv_params, np.log(skeet_prob)))
print '{}:'.format(name)
print ' Pred. Rep.: {}'.format(est)
print ''
print 'True Reputation: {}'.format(true_rep)
这会产生:
Naive Fit:
Pred. Rep.: 522562573.099
Fixed Intercept Fit:
Pred. Rep.: 4412664023.88
Long Tail Fit:
Pred. Rep.: 11728612.2783
True Reputation: 832131
答案 1 :(得分:8)
NumPy有很多功能可以拟合。对于多项式拟合,我们使用 numpy.polyfit (documentation)。
初始化您的数据集:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
data = [k.split() for k in '''0 1 4364226
1 200 269110
2 500 158824
3 1000 90368
4 2000 48609
5 3000 32604
6 5000 18921
7 10000 8618
8 25000 2802
9 50000 1000
10 100000 334'''.split('\n')]
soDF = pd.DataFrame(data, columns=('index', 'total_rep', 'users'))
soDF['total_rep'] = pd.to_numeric(soDF['total_rep'])
soDF['users'] = pd.to_numeric(soDF['users'])
soDF['log_total_rep'] = soDF['total_rep'].apply(np.log10)
soDF['log_users'] = soDF['users'].apply(np.log10)
soDF.plot(x='log_total_rep', y='log_users')
拟合二次多项式
coefficients = np.polyfit(soDF['log_total_rep'] , soDF['log_users'], 2)
print "Coefficients: ", coefficients
接下来,让我们绘制原始+适合:
polynomial = np.poly1d(coefficients)
xp = np.linspace(-2, 6, 100)
plt.plot(soDF['log_total_rep'], soDF['log_users'], '.', xp, polynomial(xp), '-')
答案 2 :(得分:3)
在阅读了Joe Kington和Jos Polfliet的优秀解释之后,我决定从我的数据中添加5个数据点,从分发的尾端(包括顶级用户),看看我是否能找到一个,好的只使用多项式拟合就足够了。
事实证明,6度多项式在分布的中心和尾中表现出色,步数更少。
下面的图表显示了数据和多项式拟合,这似乎几乎是完美的:
这是我的df,其中包含来自分发尾端的一些额外数据点:
0 1 4364226
1 200 269110
2 500 158824
3 1000 90368
4 2000 48609
5 3000 32604
6 5000 18921
7 10000 8618
8 25000 2802
9 50000 1000
10 100000 334
11 193000 100
12 261000 50
13 441000 10
14 578000 5
15 833000 1
这是我的代码:
soDF['log_total_rep'] = soDF['total_rep'].apply(np.log10)
soDF['log_users'] = soDF['users'].apply(np.log10)
coefficients = np.polyfit(soDF['log_total_rep'] , soDF['log_users'], 6)
polynomial = np.poly1d(coefficients)
print polynomial
返回此内容:
6 5 4 3 2
-0.00258 x + 0.04187 x - 0.2541 x + 0.6774 x - 0.7697 x - 0.2513 x + 6.64
图表是用以下代码完成的:
xp = np.linspace(0, 6, 100)
plt.figure(figsize=(18,6))
plt.title('Stackoverflow Reputation', fontsize =15)
plt.xlabel('Log reputation', fontsize =15)
plt.ylabel('Log probability that reputation is greater than X', fontsize = 15)
plt.plot(soDF['log_total_rep'], soDF['log_users'],'o', label ='Data')
plt.plot(xp, polynomial(xp), color='red', label='Fit', ls='--')
plt.legend(loc='upper right', fontsize = 15)
为了测试中心和尾部的适合度,我为排名为150,25和5的用户选择以下配置文件:
total_users = 4407194
def predicted_rank(total_rep):
parametric_rank_position = 10**polynomial(np.log10(total_rep))
parametric_rank_percentile = parametric_rank_position/total_users
print "Position is " + str(int(parametric_rank_position)) + ", and rank is top " + "{:.4%}".format(parametric_rank_percentile)
所以,对于Joachim Sauer来说,结果就是这样:
predicted_rank(165671)
Position is 133, and rank is top 0.0030%
关闭17个职位。对于Eric Lippert:
predicted_rank(374507)
Position is 18, and rank is top 0.0004%
关闭7个职位。对于Marc Gravell:
predicted_rank(579042)
Position is 4, and rank is top 0.0001%
关闭1个位置。为了测试分布的中心,我用我自己的测试:
predicted_rank(1242)
Position is 75961, and rank is top 1.7236%
这与75630的实际排名非常接近。