Python 分组处理

在日常数据分析时,经常会遇到需要按列分组 (groupby) 的任务,如计算某公司各部门的人数,计算各部门男女平均工资,计算不同年代的员工的平均工资等等。在进行这类运算时,Pandas 提供了 groupby 函数,大多数问题它都可以解决,但有一些问题使用 groupby 函数会略显麻烦,下面我们就这些问题展开细致的讨论。

groupby 是 pandas 中非常重要的一个函数, 主要用于数据分类和聚合计算. 其思想是“split-apply-combine”(拆分 - 应用 - 合并),如下图:

分组原理图

一、单列分组聚合

单列分组聚合是指把某一列作为键进行分组,然后对各组进行聚合运算。

它是上述分组原理的最简单应用,比如根据员工信息数据,计算各部门员工数。

问题分析:要计算各部门员工数,首先把部门作为键进行分组,然后对各组成员进行计数。

部分员工信息数据如下:

EIDNAMESURNAMEGENDERSTATEBIRTHDAYHIREDATEDEPTSALARY

1RebeccaMooreFCalifornia1974/11/202005/3/11R&D7000

2AshleyWilsonFNew York1980/7/192008/3/16Finance11000

3RachelJohnsonFNew   Mexico1970/12/172010/12/1Sales9000

4EmilySmithFTexas1985/3/72006/8/15HR7000

5AshleySmithFTexas1975/5/132004/7/30R&D16000

………………………

Python代码

import pandas as   pd

employee =   pd.read_csv("Employees.csv")

dept_emp_num =   employee.groupby('DEPT')['DEPT'].count()

print(dept_emp_num)


读取数据

分组计数

讨论:groupby(‘DEPT’) 将数据按照部门分组, count() 函数进行计数。

二、多列分组聚合

多列分组聚合是指把多列的值同时作为键进行分组,然后对各组进行聚合运算。

它和单列分组聚合类似,只是分组的键是多列组合而已。如根据员工信息数据,计算各部门男女员工的平均工资。

继续使用上例中的员工信息数据

问题分析:需要分组的键有两个,分别是部门和性别,只要把他们组合起来看作是一个键,然后当做单列分组聚合即可。

Python 代码

import pandas as pd

employee = pd.read_csv("Employees.csv")

dept_gender_salary =   employee.groupby(['DEPT','GENDER'],as_index=False).SALARY.mean()

print(dept_gender_salary)



多列分组再聚合

讨论:groupby(['DEPT','GENDER']),分组的两列以列表的形式作为参数,as_index 表示是否把分组列作为索引,True 表示作为索引,这里使用 False 表示不作为索引。使用 mean() 函数计算工资的平均值。

三、根据衍生列分组聚合

根据衍生列分组聚合,是指需要分组的键并不直接在数据中,需要根据数据计算出一列新数据,把它作为键对数据进行分组。如计算不同年代的员工的平均工资。

问题分析:员工信息数据中并没有年代这一列,因此需要根据员工的生日列计算出来,把它作为键对员工数据进行分组,然后再求工资均值。

Python 代码

import pandas as pd

import numpy as np

employee = pd.read_csv("Employees.csv")

employee['BIRTHDAY']=pd.to_datetime(employee['BIRTHDAY'])

years_salary =   employee.groupby(np.floor((employee['BIRTHDAY'].dt.year-1900)/10)).SALARY.mean()

print(years_salary)




生日列转换成日期格式


计算衍生数组并按此数组分组,再计算平均工资

讨论:年代数据在原数据中并不存在,使用 np.floor((employee['BIRTHDAY'].dt.year-1900)/10) 计算出衍生列表示年代,然后根据他分组并计算平均工资。

四、多个聚合

多个聚合,是指分组后对单列或者多列进行多种聚合。

(一)   多列单聚合

多列单聚合,指同时对多列聚合,但每列使用一种聚合方式。如:同时计算各部门员工的人数,平均工资。

问题分析:求员工人数可以对 EID 计数,求平均工资需要对工资列求均值,两列聚合但每列只用一种聚合方式。

Python 代码

import pandas as pd

employee = pd.read_csv("Employees.csv")

dept_agg =   employee.groupby('DEPT',as_index=False).agg({'EID':'count','SALARY':'mean'})

print(dept_agg.rename(columns={'EID':'NUM','SALARY':'AVG_SALARY'}))



分组并对 EID 计数,对 SALARY 求平均

重命名列名

讨论:Pandas 的 agg()函数可以完成这类任务,各列以及各列的聚合方式以字典的形式作为参数传入 agg(),聚合的列作为字典的键,聚合方式作为字典的值,从而完成聚合运算。

(二)   单列多聚合

单列多聚合,指只对一列聚合,但聚合的方式有多种。如上述问题也可以直接对工资计数并求平均,此时是对工资进行了两种聚合——计数和平均。

Python 代码

import pandas as   pd

employee =   pd.read_csv("Employees.csv")

dept_agg = employee.groupby('DEPT').SALARY.agg(['count','mean']).reset_index()

print(dept_agg.rename(columns={'count':'NUM','mean':'AVG_SALARY'}))



对 SALARY 计数并求平均

重命名列名

讨论:如果是单列的不同聚合方式,则可以把聚合方式进行组合以列表的形式作为参数传入 agg()。

(三)   多列多聚合

多列多聚合,指对多列聚合同时也包含单列多聚合的组合聚合方式。聚合方式还可以是自己定义的函数,

如:计算各部门员工人数,平均工资和最大年龄。

问题分析:计算员工人数和平均工资,是对工资列计数并求平均(单列多聚合),求最大年龄,需对生日列使用自定义的函数计算出最大年龄。

Python 代码

import pandas as   pd

import datetime

def max_age(s):

    today = datetime. datetime.today().year

    age = today-s.dt.year

    return age.max()

employee = pd.read_csv("Employees.csv")

employee['BIRTHDAY']=pd.to_datetime(employee['BIRTHDAY'])

dept_agg =   employee.groupby('DEPT').agg({'SALARY':['count','mean'],'BIRTHDAY':max_age})




dept_agg.columns   = ['NUM','AVG_SALARY','MAX_AGE']

print(dept_agg.reset_index())



函数:求最大年龄

年份

求年龄




按 DEPT 分组,根据 SALARY 计数和求均值,BIRTHDAY 使用 max_age 计算最大年龄

修改列名


讨论:这种情况,聚合列和聚合方式还是按照字典的方式传入,但当某一列需要多种聚合方式时,则需要将其组合,以列表的形式作为字典的值。

五、分组聚合值复制

分组聚合值复制,指把分组聚合的结果转换成与该组等长的列,相当于把聚合的结果复制到该组的所有行。如:为员工信息数据新增一列各部门的平均工资。

问题分析:各部门的平均工资需要按照部门分组再对工资求平均,把平均工资的值添加到对应的组,并保持数据原序。

Python 代码

import pandas as pd

employee = pd.read_csv("Employees.csv")

employee['AVG_SALARY'] =   employee.groupby('DEPT').SALARY.transform('mean')

print(employee)



按照 DEPT 分组并对 SALARY 求平均

讨论:按照部门分组后,对工资求均值。transform() 函数在组内求聚合值后会按照原索引的顺序返回结果,可以自动按照索引添加结果,从而保证原数据顺序不变。

六、分组子集处理

分组应用:指分组后对各组进行一些非聚合运算。比如分组排序,分组后不再关心聚合的结果,而是关心组内记录的顺序。如:将各部门按照入职时间从早到晚进行排序 。

问题分析:按照部门分组后,不再关心分组后的聚合结果,而是关心员工的入职时间顺序。分组后,对各组进行循环同时对组内成员按照入职时间排序就可以了。

Python 代码

import pandas as pd

employee = pd.read_csv("Employees.csv")

employee['HIREDATE']=pd.to_datetime(employee['HIREDATE'])

employee_new =   employee.groupby('DEPT',as_index=False).apply(lambda   x:x.sort_values('HIREDATE')).reset_index(drop=True)

print(employee_new)



修改入职时间格式


按 DEPT 分组,并对各组按照 HIREDATE 排序,最后重置索引


讨论:分组后需要对组内成员排序,可以使用 apply()函数结合 lambda 的方式,其中 lambda 表达式是对各组循环,使用 sort_values() 函数在组内部再排序,返回组内排序的结果。

简单的运算使用 lambda 函数计算,但有时会遇到比较复杂的计算,如:计算各部门年龄最大的员工和年龄最小的员工的工资差。

问题分析:首先需按照部门分组,分组后还需要找到年龄最大的员工和年龄最小的员工的记录,然后才能计算工资差。

Python 代码

import pandas as pd

def salary_diff(g):

    max_age =   g['BIRTHDAY'].idxmin()

    min_age =   g['BIRTHDAY'].idxmax()

    diff =   g.loc[max_age]['SALARY']-g.loc[min_age]['SALARY']

    return diff

employee = pd.read_csv("Employees.csv")

employee['BIRTHDAY']=pd.to_datetime(employee['BIRTHDAY'])

salary_diff = employee.groupby('DEPT').apply(salary_diff)

print(salary_diff)


函数:计算各组工资差

年龄最大的索引

年龄最小的索引

计算工资差




按 DEPT 分组并使用自定义函数计算


讨论:使用 apply()结合自定义函数的方式。其中 apply() 会把分组的结果作为参数传入自定义函数。salary_diff() 函数是自定义函数,g 实质上就是 pandas 的 DataFrame 格式的数据框,这里是分组的结果。对它计算最大年龄和最小年龄的索引后,找到工资字段计算差即得到结果。

思考:

由上述讨论可见,熟练掌握 Pandas 的这些 groupby 方法对我们进行数据分析是特别有帮助的。


下面我们以 stack overflow 网站上的一些实际问题来进一步了解 groupby。

七、按位置分组

按位置分组,指不以某列作为键分组,而是以记录的位置作为键来分组。比如将数据每三行分到相同组或者按照位置分成奇数位置一组,偶数位置一组等。举例如下:

source:https://stackoverflow.com/questions/59110612/pandas-groupby-mode-every-n-rows

数据片段如下:

time                       a                       b

0                          0.5                    -2.0

1                          0.5                    -2.0

2                          0.1                    -1.0

3                          0.1                    -1.0

4                          0.1                    -1.0

5                          0.5                    -1.0

6                          0.5                    -1.0

7                          0.5                    -3.0

8                          0.5                    -1.0

希望每三行分成一组,并把众数作为该组的结果。理想的结果如下:

time                       a                       b

2                          0.5                    -2.0

5                          0.1                    -1.0

8                          0.5                    -1.0

问题分析:该问题的分组与现有的列没有关系,只与位置相关,因此需要衍生出一列作为分组依据,按位置做整数乘法即得到衍生列,然后据此分组即可。

Python 代码

import pandas as pd

import numpy as np

data = pd.read_csv("group3.txt",sep='\t')

res = data.groupby(np.arange(len(data)) //   3).agg(lambda x: x.mode().iloc[-1])

print(res)




按照衍生列分组,使用 agg 结合 lambda 的方式得到众数,取各组各列的最后 1 个众数作为结果

讨论:衍生列计算方式为 np.arange(len(data)) // 3,其结果是 [0 0 0 1 1 1 2 2 2],把它作为键进行分组就可以把数据分成每三行一组。而 agg(lambda x: x.mode()) 则是将各组的各列分别求众数,如第一组 time 的众数为 [0,1,2] 而 a 和 b 的众数分别是 [0.5] 和[-2.0]分别取最后 1 个众数 iloc[-1]即得到想要的结果。

八、值变化分组

值变化分组,指在有序的数据中,发生数据变化时就分出一个新组。举例如下:

source:https://stackoverflow.com/questions/41620920/groupby-conditional-sum-of-adjacent-rows-pandas

数据片段如下:

      duration  location  user

0        10    house    A

1         5    house    A

2         5      gym    A

3         4      gym    B

4        10     shop    B

5         4      gym    B

6         6      gym    B

按照 user 分组后,各组当 location 连续相同时对 duration 进行求和,location 变化时则重新求和。理想结果如下:

   duration  location   user

        15    house    A

         5      gym    A

         4      gym    B

        10     shop    B

        10      gym    B

问题分析:location 列的顺序很重要,连续相同时可以视为一组,当变化时则重新分一组,如 user=B 时,第 4 行 (索引为 3) 的 location 为 [gym,shop,gym,gym], 不可以把其中的 3 个 gym 分到 1 组,而应该把第一个 gym 单独作为 1 组,shop 与 gym 不同,值发生了变化,把 shop 分到下一组,后面两个 gym 没有值变化,可以分到同一组,分组的结果为[[gym],[shop],[gym,gym]],所以这里不可以使用 df.groupby(['user','location']).duration.sum() 来计算结果,而是要想办法生成一个衍生列作为分组依据。

代码如下:

import pandas as pd

df = pd.DataFrame({'user' : ['A', 'A', 'A', 'B', 'B',   'B','B'],

                'location' : ['house','house','gym','gym','shop','gym','gym'],

                'duration':[10,5,5,4,10,4,6]})

derive = (df.location !=   df.location.shift()).cumsum()

res = df.groupby(['user', 'location', derive],   as_index=False, sort=False)['duration'].sum()

print(res)


生成数据




创造衍生列

按照 user,location 和衍生列分组,对 duraton 求和


讨论:衍生列 derive 是当 location 与前者不同时进行累加,得到 [1 1 2 2 3 4 4]。然后按照 user,location 和该数列分组,再对 duration 求和。

九、条件变化分组

条件变化分组:指在有序的数据中,当满足某一条件时重新分组。举例如下:

source:https://stackoverflow.com/questions/62461647/choose-random-rows-in-pandas-datafram

数据片段如下:

ID          code

333_c_132   x

333_c_132   n06

333_c_132   n36

333_c_132   n60

333_c_132   n72

333_c_132   n84

333_c_132   n96

333_c_132   n108

333_c_132   n120

999_c_133   x

999_c_133   n06

999_c_133   n12

999_c_133   n24

998_c_134   x

998_c_134   n06

998_c_134   n12

998_c_134   n18

998_c_134   n36

997_c_135   x

997_c_135   n06

997_c_135   n12

997_c_135   n24

997_c_135   n36

996_c_136   x

996_c_136   n06

996_c_136   n12

996_c_136   n18

996_c_136   n24

996_c_136   n36

995_c_137   x

希望从 code 列的每两个 x 中间随机取一行

理想结果形式如下:

333_c_132   n06

999_c_133   n12

998_c_134   n18

997_c_135   n36

996_c_136   n18

问题分析:取两个 x 之间的随机一条记录,可以转化成每当 code 等于 x 时开始新的一组,不等于 x 时分组不变,然后从该组中随机取一行。因此这里还是需要生成衍生列,把它作为键分组才能完成任务。

代码如下:

import pandas as pd

df = pd.read_csv("data.txt")

derive = df.code.eq('x').cumsum()

res=df[df.code.ne('x')].groupby(derive).apply(lambda   x : x.sample(1))

res=res.reset_index(level=0, drop=True)

print(res) 



生成衍生列

根据衍生列分组,使用 apply 结合 lambda 的方式随机抽样

重置索引


讨论:code.eq(x) 表示 code 等于 x 时为 True,其余为 False,cumsum()表示对其累加,生成的衍生列为 [1 1 1 1 1 1 1 1 1 2 2…],过滤掉等于 x 的列再根据该列进行分组并抽样即可。

思考:

前面所有的例子都是将原集合根据某个条件,将数据划分成若干个子集,且满足以下两点:

1)没有空子集

2)原集合的任何成员都属于且只属于某一个子集

我们称这种划分方式为完全划分。那么有没有不完全划分呢?

来看下面这几个例子

十、对位分组

对位分组,指先罗列出一个基准集合,然后将待分组集合成员的某个属性(字段或表达式)与基准集合成员比较,相同者则分到一个子集中,最后拆分出来的子集数量和基准集合成员数是相同的。对位分组有三个特点:

1)可能出现空子集(比如基准集合的某些成员在待分组集合中并不存在);

2)可能有待分组集合成员未被分到任何子集(比如有些不重要的成员未被列入基准集合);

3)每个成员最多只出现在一个子集中。

(一)出现空子集

公司统计各部门男女人数,如果某个部门没有男员工或者没有女员工,则将该部门的男员工人数或女员工人数填为 0。

问题分析:如果直接按照部门和性别分组,则如果某个部门没有女员工或没有男员工时,该部门将只被分成 1 组,就会丢失掉缺少的性别的统计信息,因此不可以直接 groupby([‘DEPT’,’GENDER’])。很容易想到的方案就是,先按部门分组,罗列出 [男, 女] 的基准集合,使用左连接 (left join) 的方式与各组连接,再对连接后的结果按照性别分组,最后汇总结果,这样就能保证分组的结果总会有 [男, 女] 了。

Python 代码

import pandas as pd

def align_group(g,l,by):

    d = pd.DataFrame(l,columns=[by])

    m =   pd.merge(d,g,on=by,how='left')

return m.groupby(by,sort=False)

employee = pd.read_csv("Employees.csv")

l = ['M','F']

res = employee.groupby('DEPT').apply(lambda   x:align_group(x, l, 'GENDER').apply(lambda s:s.EID.count()))

print(res)


函数,对位分组

生成对照的 dataframe

利用 merge 完成对位运算

分组


指定序列

按 DEPT 分组,再对各组使用函数对位分组,对 EID 进行计数


讨论:

自定义函数 align_group,使用 merge()函数完成罗列集合与待分组集合的 left join,再按 merge 的列进行分组。按部门分组后,使用 apply() 结合 lambda 表达式的方式对每组使用自定义函数对位分组,最后对 EID 列计数得到最终结果。(注意:这里不可以对 GENDER 计数,因为 merge 时 GENDER 的成员都被保留了,如果有空子集时,对它计数结果将是 1,而其他列(比如 EID), 在 left join 时会是空值,所以对 EID 计数结果是 0)。

(二)有待分组集合成员未被分到任何子集

按指定的部门 ['Administration', 'HR', 'Marketing', 'Sales'] 分组,只查询这几个部门的人数且部门先后顺序保持不变。

问题分析:与出现空子集的情况类似,此时也可以使用 left join 的方式,将不在预先罗列的集合成员排除掉,只保留罗列集合中的成员。

代码如下:

import pandas as pd

def align_group(g,l,by):

    d =   pd.DataFrame(l,columns=[by])

    m =   pd.merge(d,g,on=by,how='left')

    return   m.groupby(by,sort=False)

employee = pd.read_csv("Employees.csv")

sub_dept = ['Administration', 'HR', 'Marketing',   'Sales']

res =   align_group(employee,sub_dept,'DEPT').apply(lambda x:x.EID.count())

print(res)


函数,对位分组





指定顺序的部门子集

使用对位分组函数分组,再对 EID 计数

讨论:Pandas 不直接支持对位分组的功能,因此完成起来成本就会比较高,而且使用 merge 函数也会导致运行效率低下。

十一、枚举分组

枚举分组:事先指定一组条件,将待分组集合的成员作为参数计算这批条件,条件成立者被划分到与该条件对应的一个子集中,结果集的子集和事先指定的条件一一对应。枚举分组的特点:允许集合成员重复出现在不同的子集中。

举例如下:

按在公司的工龄将员工分组统计每组的男女员工人数(分组条件重合时,列出所有满足条件的员工,分组的条件是 [工龄 <5 年,5 年 <= 工龄 <10 年,工龄 >=10 年,工龄 >=15 年])

问题分析:工龄 >=10 年和工龄 >=15 年两个条件有重复的区间,即工龄大于 15 年的员工,其工龄也一定大于 10 年,这时如果使用构造衍生列的方式来完成,将无法使同一个成员重复出现在两个分组中,因此需要考虑每个条件都分一次组,然后找出满足条件的组,最后再汇总。

import pandas as pd

import datetime

def eval_g(dd:dict,ss:str):

    return   eval(ss,dd)   

emp_file = 'E:\\txt\\employee.txt'

emp_info = pd.read_csv(emp_file,sep='\t')

employed_list = ['Within five years','Five to ten   years','More than ten years','Over fifteen years']

employed_str_list =   ["(s<5)","(s>=5) &   (s<10)","(s>=10)","(s>=15)"]

today = datetime.datetime.today().year

arr = pd.to_datetime(emp_info['HIREDATE'])

employed = today-arr.dt.year

emp_info['EMPLOYED']=employed

dd = {'s':emp_info['EMPLOYED']}

group_cond = []

for n in range(len(employed_str_list)):

    emp_g =   emp_info.groupby(eval_g(dd,employed_str_list[n]))

    emp_g_index   = [index for index in emp_g.size().index]

    if True not   in emp_g_index:

          female_emp=0

          male_emp=0

    else:

        group =   emp_g.get_group(True)

        sum_emp   = len(group)

          female_emp = len(group[group['GENDER']=='F'])

          male_emp = sum_emp-female_emp

      group_cond.append([employed_list[n],male_emp,female_emp])

group_df =   pd.DataFrame(group_cond,columns=['EMPLOYED','MALE','FEMALE'])

print(group_df)



函数,字符串转表达式






分组条件




计算入职时间




循环分组条件

按分组条件分组

分组索引

如果没有满足条件的成员

男女员工数为 0


满足条件

获取分组

计算男女员工人数





汇总各个分组条件的计算结果

讨论:EMPLOYED 是根据入职时间 HIREDATE 新增加的一列,表示工龄。自定义函数 eval_g(),是把分组的条件转换成表达式,比如当条件是 s<5 时,eval_g(dd,ss)的表达式就是 emp_info['EMPLOYED']<5,根据这个衍生列来对数据分组。对分组条件进行循环,按该衍生列分成两组,get_group(True) 表示取满足条件的组,最后把所有满足条件的结果使用 concat() 函数汇总。

总结

Python 在进行分组处理时,多数情况可以比较优雅的处理,但在处理有序分组时,如值变化分组、条件变化分组时则需要自己想办法生成满足分组条件的衍生列,略显麻烦。对位分组和枚举分组的两种情况更是糟糕,需要自己想办法去绕,要么使用 merge 运算,要么多次分组,使分组的成本变得很高,这样看来,Pandas 的分组运算还有其局限性。

对于分组运算,相比之下,esProc SPL 处理的更完善。 esProc 是专业的数据计算引擎,SPL 提供了丰富的分组运算,可以方便的完成上述任务,代码风格的一致程度也更好。

两个分组运算函数 groups()和 group(),分别实现分组聚合和分组子集,可以比 Python 更简洁地解决前面六个常规分组问题:

分组子集运算

对于这六个简单分组计算,Python 的分组计算方法同样方便。但涉及了很多其他函数,如 agg,transform,apply,lambda 表达式甚至是自定义函数等等,代码风格差别比较大。而 SPL 则基本保持了 groups(x;y) 或者是 group(x).(y) 这样统一的代码风格。

对于问题七、八、九,Python 就略显烦琐,需想办法生成衍生列,而 SPL 本身基于有序集合设计,提供了有序分组的选项,仍可以优雅的保持简单运算时的代码风格。

问题SPL代码简单说明

根据分组后直接聚合还是分组后针对子集计算,灵活选择 groups 和 group 函数。

最后两个问题,对位分组和枚举分组,确实有点难为 Python 了,不过不管是使用 merge 函数绕还是多次分组,总算是完成了任务。而 SPL 提供了专门的对位分组函数 align()和枚举分组函数 enum(),可以继续优雅。


有成员被分到不同子集

需要提到的是,Python 还有一个致命缺点——大数据(无法一次性读入内存)分组,它涉及到外存读写和 hash 分组,对于非专业的程序员来说,使用 Python 完成这个任务几乎是不可能的。有兴趣可以参考以下文章:

Python 如何处理大文件

这里介绍了 Python 处理大数据存在的问题(包括大数据分组),也简单介绍了 esProc SPL 中的游标系统,其中 group 和 groupx() 函数仍然可以优雅的完成大数据分组任务。

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

推荐阅读更多精彩内容