Data_StackExchange_Python

本文用到的包为

import numpy as np
import matplotlib.pyplot as plt
import networkx as nx
from collections import defaultdict
from collections import Counter
from numpy import linalg as LA
import statsmodels.api as sm
import matplotlib.cm as cm
from datetime import datetime as dt
import sys
from os import listdir
from scipy.stats.stats import pearsonr
from matplotlib.dates import YearLocator

StackExchange(以下简称SE)是世界上最大的专业性问答社区之一。最早只有一个StackOverflow,后来慢慢发展出其他的问答社区,现在一共有一百多社区。在这里可以看到所有社区。

图1. StackExchange的一部分社区
图1. StackExchange的一部分社区

[这个]问答给出了SE历史数据的下载地址。本文给出对SE数据的初步处理示例。

首先定义一些数据处理函数:

def dailyQA(site):
    F = defaultdict(lambda:[0,0])
    path='/Users/csid/Documents/bigdata/stackexchange/unzip/'
    filename = path + site + '/Posts.xml'
    with open(filename,'r') as f:
        for line in f:
            try:
                label = line.split('PostTypeId=')[1][1:2]
                day = line.split('CreationDate=')[1][1:11]
                if label == '1':
                    F[day][0]+=1
                if label == '2':
                    F[day][1]+=1
            except:
                pass
    return F

#plot the monthly growth of sites in terms of Na and Nq
def plotMonth(site,ax,col):
    M=defaultdict(lambda:np.array([0,0]))
    f=F[site]
    for i in f:
        M[i[:7]]+=np.array(f[i])
    ms=sorted(M.keys())[1:-1]
    if len(ms)>3:
        x,y = np.array([M[i] for i in ms]).T
        mm=[dt.strptime(j,'%Y-%m') for j in ms]
        #ax.vlines(mm[0], x[0], y[0],color=col,linestyle='-')
        ax.fill_between(mm, x, y,color=col, alpha=0.1)
        ax.plot(mm,x,color="white",linestyle='-',marker='',alpha=0.1)
        ax.plot(mm,y,color="white",linestyle='-',marker='',alpha=0.1)

def plotMonthSpecial(site,ax,col):
    M=defaultdict(lambda:np.array([0,0]))
    f=F[site]
    for i in f:
        M[i[:7]]+=np.array(f[i])
    ms=sorted(M.keys())[2:-1]
    x,y = np.array([M[i] for i in ms]).T
    mm=[dt.strptime(j,'%Y-%m') for j in ms]
    ax.vlines(mm[0], x[0], y[0],color=col,linestyle='-')
    ax.plot(mm,x,color=col,linestyle='-',marker='')
    ax.plot(mm,y,color=col,linestyle='-',marker='')

通过下列代码得到每个社区每天新增的问题和答案数

path='/Users/csid/Documents/bigdata/stackexchange/unzip/'
sites = [ f for f in listdir(path) if f[-1]=='m']
F={}
for i in sites:
    flushPrint(sites.index(i))
    F[i] = dailyQA(i)

好的可视化,需要层次分明,所以在绘制各个社区问答数量增长曲线时,往往需要排序来决定绘制的先后叠加顺序。下列代码将各个社区按照总的问答数量排序。

# plot good sites at first then plot bad sites
S={}
for i in sites:
    q,a=zip(*F[i].values())
    S[i]=sum(q),sum(a)
rsites=[i for i,j in sorted(S.items(),key=lambda x:-x[1][0])]

然后就可以绘制图2了

图1. StackExchange的一部分社区
图1. StackExchange的一部分社区

每条带子是一个社区,上界是每月新增答案数,下界是每月新增问题数。一共有110个社区。颜色代表社区的问答总数(取对数再减去5)。我们还可以选择性地标示出某些社区,例如本图中标示出了物理类(蓝色)和烹调类(深绿色)两个社区。

绘制代码为

fig = plt.figure(figsize=(12, 5),facecolor='white')
ax = plt.subplot(111)
years = YearLocator()
cmap = cm.get_cmap('PiYG', 10)
for i in rsites:
    c = int(np.log(S[i][0])-5)
    plotMonth(i,ax,cmap(c))
plotMonthSpecial('physics.stackexchange.com',ax,'RoyalBlue')
plotMonthSpecial('cooking.stackexchange.com',ax,'DarkOliveGreen')
ax.set_yscale('log')
ax.set_ylim(1,10**6)
ax.set_xlabel('Time')
ax.set_ylabel('Monthly increased N of Q&A')
ax.xaxis.set_major_locator(years)
smm = plt.cm.ScalarMappable(cmap=cmap, norm=plt.Normalize(vmin=0, vmax=10)) 
smm._A = []
cbaxes = fig.add_axes([0.15, 0.85, 0.5, 0.015]) 
cbar = plt.colorbar(smm,cax=cbaxes,orientation='horizontal')
plt.show()

接下来,我们考虑使用节点到源和汇的流距离构造一个相空间,分析用户在这个相空间中游走的轨迹产生的角度熵(把在两个节点间的每一步跳跃合并到一个原点上,考察角度的分布)与社区可持续发展之间的关系。我们的假设是,用户游走的熵越大,说明用户越有创造性,对问答社区的长期发展也越有利。

首先要定义一系列函数

def userDailyAnswers(site):
    C={}
    filename = path + site + '/Posts.xml'
    with open(filename,'r') as f:
        for line in f:
            try:
                label = line.split('PostTypeId=')[1][1:2]
                if label == '2':
                    date = line.split('CreationDate=')[1][1:11]
                    time = line.split('CreationDate=')[1][12:20]
                    author = int(line.split('OwnerUserId=')[1].split(r'"')[1])
                    questionID = int(line.split('ParentId=')[1].split(r'"')[1])
                    if date in C:
                        if author in C[date]:
                            C[date][author]+=[(time,questionID)]
                        else:
                            C[date][author]=[(time,questionID)]
                    else:
                        C[date]={author:[(time,questionID)]}
            except:
                pass
    return C

# calculate entropy of path angles
def entropy(G,O,K,T):
    angles=[]
    for i,j in G.edges():
        #wi = G[i][j]['weight']
        dx,dy = np.array([O[j],K[j]])-np.array([O[i],K[i]])
        dis = LA.norm(np.array([O[j],K[j]])-np.array([O[i],K[i]]))
        if dy>=0:
            angle = np.round(180*np.arccos(dx/dis)/np.pi,1)
        else:
            angle = 360-np.round(180*np.arccos(dx/dis)/np.pi,1)
        angles.append(angle)
    l = len(angles)
    ps=np.array(Counter(angles).values())
    ps=ps/float(ps.sum())
    #ent = -(ps*np.log(ps)).sum()/np.log(l)
    ent = -(ps*np.log(ps)).sum()
    return ent


def getSiteFlowdata(site):
    C=userDailyAnswers(site)
    days=sorted(C.keys())
    E=defaultdict(lambda:0)
    n=0
    maxuser=100
    for day in days[len(days)/2:]:
        d = C[day]
        f = sorted(d.items(),key=lambda x:x[1])
        for i,j in f:
            if n<maxuser:
                n+=1
                q = [p for o,p in j]
                q = ['source']+q+['sink']
                for a,b in zip(q[:-1],q[1:]):
                    E[(a,b)]+=1
    
    G=nx.DiGraph()
    for x,y in E:
        w = E[(x,y)]
        G.add_edge(x,y,weight=w)
    O = flowDistanceFromSource(G)
    K = flowDistanceToSink(G)
    T = G.out_degree(weight='weight')
    return G,O,K,T

# orthogonal okplot
def okplot(G,O,K,T):
    plt.plot([0,4],[0,4],'r-',alpha=0.5)
    for i,j in G.edges():
        wi = G[i][j]['weight']
        x1,y1=O[i],K[i]
        x2,y2=O[j],K[j]
        dx=x2-x1
        dy=y2-y1
        plt.arrow(x1, y1, dx, dy, head_width=0.1, head_length=0.2, fc='gray', ec='gray',alpha=0.2)
        #plt.text(x2,y2,wi,color='brown')
    plt.xlabel(L_{oi},size=16)
    plt.ylabel(L_{ik},size=16)

# rescaled orthogonal okplot
def rescaledokplot(G,O,K,T):
    r = 0
    Dx=0;Dy=0
    tr=0
    for i,j in G.edges():
        wi = G[i][j]['weight']
        x1,y1=O[i],K[i]
        x2,y2=O[j],K[j]
        dx=x2-x1
        dy=y2-y1
        Dx+=dx
        Dy+=dy
        rr = np.sqrt(dx**2+dy**2)
        tr+=rr
        if rr>r:
            r=rr
        plt.arrow(0, 0, dx, dy, head_width=0.05, head_length=0.1, fc='gray', ec='gray',alpha=0.1)
    plt.arrow(0, 0, Dx/float(tr), Dy/float(tr), head_width=0.1, 
              head_length=0.2, fc='red', ec='red',alpha=0.7)
    lim=2
    plt.xlim(-lim,lim)
    plt.ylim(-lim,lim)

接着就可以比较物理和烹调这两个规模相近的社区,取其总天数一半时的一百个用户产生的游走轨迹的角度熵

i='physics.stackexchange.com'
j='cooking.stackexchange.com'
G1,O1,K1,T1=getSiteFlowdata(i)
G2,O2,K2,T2=getSiteFlowdata(j)
# okplot demo
fig = plt.figure(figsize=(12, 6),facecolor='white')
ax = plt.subplot(121)
okplot(G1,O1,K1,T1)
ax = plt.subplot(122)
okplot(G2,O2,K2,T2)
plt.tight_layout()
plt.show()

得到下图

物理和烹调社区一百个用户的游走轨迹,横轴是问题节点离源的距离,纵轴是其到汇的距离。问题节点在此图中不显示,只以箭头显示用户在节点之间的跳跃。
物理和烹调社区一百个用户的游走轨迹,横轴是问题节点离源的距离,纵轴是其到汇的距离。问题节点在此图中不显示,只以箭头显示用户在节点之间的跳跃。
把所有用户的所有一步跳跃矢量合并到一个原点上以计算角度熵,红色代表矢量合并的结果。矢量合并的结果一定是一单位长的箭头以45度角指向右下方,因为所有用户的所有游走都是从源开始,到汇结束。
把所有用户的所有一步跳跃矢量合并到一个原点上以计算角度熵,红色代表矢量合并的结果。矢量合并的结果一定是一单位长的箭头以45度角指向右下方,因为所有用户的所有游走都是从源开始,到汇结束。

可以通过下列代码

entropy(G1,O1,K1,T1),entropy(G2,O2,K2,T2)

来计算得到两个社区的熵分别为3.47和2.67。物理社区的熵更大,实际发展也更好,验证了我们的假设。

接下来,我们考察所有社区在发展一半时的一百个用户记录,以此预测其最终发展规模

# construct network and calculate path entropy
D={}
for site in sites:
    if site=='ebooks.stackexchange.com' or site=='stackoverflow.com':
        continue
    flushPrint(sites.index(site))
    C=userDailyAnswers(site)
    days=sorted(C.keys())
    E=defaultdict(lambda:0)
    n=0
    maxuser=100
    for day in days[len(days)/2:]:
        d = C[day]
        f = sorted(d.items(),key=lambda x:x[1])
        for i,j in f:
            if n<maxuser:
                n+=1
                q = [p for o,p in j]
                q = ['source']+q+['sink']
                for a,b in zip(q[:-1],q[1:]):
                    E[(a,b)]+=1
    G=nx.DiGraph()
    for x,y in E:
        w = E[(x,y)]
        G.add_edge(x,y,weight=w)
    O = flowDistanceFromSource(G)
    K = flowDistanceToSink(G)
    T = G.out_degree(weight='weight')
    D[site]=entropy(G,O,K,T)

l,a,q=np.array([(D[i],S[i][0],S[i][1]) for i in D if i in S and i!='aviation.stackexchange.com']).T
cs,beta,r2=OLSRegressFit(l,np.log(q))
fig = plt.figure(figsize=(8, 8))
plt.plot(l,q,linestyle='',marker='s',color='RoyalBlue',label='N of Questions')
plt.plot(l,a,linestyle='',marker='^',color='Chocolate',label='N of Answers')
plt.plot(l,np.exp(cs+beta*l),linestyle='-',marker='',color='Brown')
plt.yscale('log')
plt.legend(loc=1,numpoints=1)
plt.xlabel('Entropy of angles', size=16)
plt.ylabel('N of Questions & Answers', size=16)
plt.show()

得到下图

角度熵与社区规模之间的相关
角度熵与社区规模之间的相关

考察其皮尔逊相关系数

pearsonr(l,np.log(q))

得到0.42,p-value小于0.001。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 随着Web 2.0时代的到来,网络用户之间的交互关系开始被重视,网络社区中以用户为中心的互动也变得愈发频繁。不少注...
    medisol阅读 6,151评论 0 26
  • 能走开的都不是最爱,走不开的是命定。
    清顾阅读 124评论 0 1
  • 昨天晚上,你对我说:“妈妈,我想再补一门课,我只补了一门新概念英语是不够的。”孩子,妈妈很为你这种努力求上进的学习...
    生活馈赠与我阅读 266评论 2 3
  • 我国发现的最早的钓鱼文物是陕西省西安半坡村发现的骨制鱼钓和黑龙江小兴凯湖岗上出土的骨制鱼钩,距今大约有六千...
    文澄澈阅读 462评论 0 3