【问题标题】:Reduce daily status table to only contain status changes减少每日状态表以仅包含状态更改
【发布时间】:2021-08-05 18:45:14
【问题描述】:

我有一个大型(5.7 亿行)每日状态表,可供 10 万以上的用户使用。目前它在 MySQL(或 CSV)中。该表包含三列:user_id、状态和日期。理想情况下,我想将表简化为一个新表,其中包括每个状态周期的 user_id、status、start_date、end_date(其中一个周期至少是一个日期)。

挑战在于用户可以在状态之间来回切换:我不能假设一个时期被定义为给定状态的最小日期和最大日期之间的差异。

我可以使用 MySQL、Python 或终端 (mac) 中的解决方案。

当前数据:

user_id, status, date
1, GRE, 2018-09-02
1, GRE, 2018-09-03
1, PRO, 2018-09-04
1, PRO, 2018-09-05
1, PRO, 2018-09-06
1, GRE, 2018-09-07
1, GRE, 2018-09-08
1, GRE, 2018-09-09
1, GRE, 2018-09-10

所需的新格式:

user_id, status, start_date, end_date
1, GRE, 2018-09-02, 2018-09-03
1, PRO, 2018-09-04, 2018-09-06
1, GRE, 2018-09-07, 2018-09-10

【问题讨论】:

  • 我用的是8.0.15版
  • 没有唯一的主键。加载 CSV 文件(从外部源)后,我只在 user_id 上添加了一个索引。如果您的解决方案需要,我可以将唯一索引列添加到 CSV 并重新加载数据。
  • 此时它是日期和 user_id 的组合(每个用户每天只有一条记录 - 从每个用户的开始日期开始)。
  • 所有这些人都花时间来帮助你。我认为不以某种方式承认他们的努力很奇怪。
  • 我不反对,非常感谢大家付出的时间和努力。我只是认为最好的做法是避免“不相关”的 cmets。作为记录,我选择了你提供的 MySQL 解决方案,因为它更容易实现(尽管查询需要很多小时才能运行)。我相信 Python 的建议也会奏效。

标签: python mysql csv terminal


【解决方案1】:

考虑以下几点:

架构

DROP TABLE IF EXISTS my_table;

CREATE TABLE my_table
(user_id INT NOT NULL
,status CHAR(3) NOT NULL
,date DATE NOT NULL
,PRIMARY KEY(user_id,date)
);

INSERT INTO my_table VALUES
(1, 'GRE', '2018-09-02'),
(1, 'GRE', '2018-09-03'),
(1, 'PRO', '2018-09-04'),
(1, 'PRO', '2018-09-05'),
(1, 'PRO', '2018-09-06'),
(1, 'GRE', '2018-09-07'),
(1, 'GRE', '2018-09-08'),
(1, 'GRE', '2018-09-09'),
(1, 'GRE', '2018-09-10'),

(2, 'GRE', '2018-09-02'),
(2, 'GRE', '2018-09-03'),
(2, 'PRO', '2018-09-04'),
(2, 'PRO', '2018-09-05'),
(2, 'PRO', '2018-09-06'),
(2, 'GRE', '2018-09-07'),
(2, 'GRE', '2018-09-08'),
(2, 'GRE', '2018-09-09'),
(2, 'GRE', '2018-09-10');

查询

WITH t AS (
  SELECT user_id
       , status
       , date
       , DENSE_RANK() OVER (PARTITION BY user_id ORDER BY date) 
       - DENSE_RANK() OVER (PARTITION BY user_id,status ORDER BY DATE) grp
    FROM my_table
    )
SELECT t.user_id
     , t.status
     , MIN(t.date) start
     , MAX(t.date) finish
  FROM t
 GROUP  
    BY user_id
     , status
     , grp
 ORDER  
    BY user_id
     , start;
     

+---------+--------+------------+------------+
| user_id | status | start      | finish     |
+---------+--------+------------+------------+
|       1 | GRE    | 2018-09-02 | 2018-09-03 |
|       1 | PRO    | 2018-09-04 | 2018-09-06 |
|       1 | GRE    | 2018-09-07 | 2018-09-10 |
|       2 | GRE    | 2018-09-02 | 2018-09-03 |
|       2 | PRO    | 2018-09-04 | 2018-09-06 |
|       2 | GRE    | 2018-09-07 | 2018-09-10 |
+---------+--------+------------+------------+

【讨论】:

    【解决方案2】:

    使用 Python 熊猫

    import pandas as pd
    
    def format_data(filenm):
        # Load CSV to Dataframe
        df = pd.read_csv(filenm, skipinitialspace=True)
        
        # Add a column that allows grouping consecutive rows with same status
        #  (note: technique from https://towardsdatascience.com/pandas-dataframe-group-by-consecutive-same-values-128913875dba)
        df['same'] = (df['status'] != df['status'].shift()).cumsum()
        
        # Get dates fro grouped user id and rows with same status
        g = df.groupby(['user_id', 'same'])['date']
        
        # Add start and stop dates based upon above grouping
        df = df.assign(startdate = g.transform('first'), enddate = g.transform('last'))
        
        # Return Dataframe with userid, status, startdate, enddate
        return df.groupby(['user_id', 'same']).agg({
            'status': 'first',                     # use first item in group
            'startdate': 'first',                  # use first item in group
            'enddate': 'last'                      # use last item in group
        }).reset_index().drop('same', 1)         # Drop 'same' column (only used for grouping)
    

    用法

    result = format_data('myfile.txt')
    

    输入 myfile.txt

    (在 OP 数据中添加了第二个用户 ID 以进行演示)

    user_id, status, date
    1, GRE, 2018-09-02
    1, GRE, 2018-09-03
    1, PRO, 2018-09-04
    1, PRO, 2018-09-05
    1, PRO, 2018-09-06
    1, GRE, 2018-09-07
    1, GRE, 2018-09-08
    1, GRE, 2018-09-09
    1, GRE, 2018-09-10
    2, GRE, 2018-09-08
    2, GRE, 2018-09-09
    2, PRO, 2018-09-10
    

    结果

        user_id status  startdate   enddate
    0   1   GRE 2018-09-02  2018-09-03
    1   1   PRO 2018-09-04  2018-09-06
    2   1   GRE 2018-09-07  2018-09-10
    3   2   GRE 2018-09-08  2018-09-09
    4   2   PRO 2018-09-10  2018-09-10
    

    【讨论】:

      【解决方案3】:

      首先,我们需要向您的数据框添加另一列,以区分数据框开头和结尾的“GRE”状态(以及类似的其他重复状态)。换句话说,需要跟踪状态何时发生变化。

      df['track_id'] = (df.status.shift() != df.status).cumsum()
      

      df:

         user_id status        date  track_id
      0        1    GRE  2018-09-02         1
      1        1    GRE  2018-09-03         1
      2        1    PRO  2018-09-04         2
      3        1    PRO  2018-09-05         2
      4        1    PRO  2018-09-06         2
      5        1    GRE  2018-09-07         3
      6        1    GRE  2018-09-08         3
      7        1    GRE  2018-09-09         3
      8        1    GRE  2018-09-10         3
      

      然后,对于status,track_id 的每个组合,使用groupby() 取出第一行和最后一行

      df_f = df.groupby(['status','track_id']).first().reset_index()
      df_l = df.groupby(['status','track_id']).last().reset_index()
      

      df_f:

        status  track_id  user_id        date
      0    GRE         1        1  2018-09-02
      1    GRE         3        1  2018-09-07
      2    PRO         2        1  2018-09-04
      

      df_l:

        status  track_id  user_id        date
      0    GRE         1        1  2018-09-03
      1    GRE         3        1  2018-09-10
      2    PRO         2        1  2018-09-06
      

      df_fdf_l中的date列分别重命名为start_dateend_date

      df_f.rename(columns = {'date':'start_date'}, inplace=True)
      df_l.rename(columns = {'date':'end_date'}, inplace=True)
      

      最后合并两个数据框以获得您想要的格式:

      final_df = df_f.merge(df_l, on=['status','track_id','user_id'])
      

      final_df:

      final_df.sort_values('track_id')
        status  track_id  user_id  start_date    end_date
      0    GRE         1        1  2018-09-02  2018-09-03
      2    PRO         2        1  2018-09-04  2018-09-06
      1    GRE         3        1  2018-09-07  2018-09-10
      

      请注意,我对user_id = 1 进行了整个操作,只是为了给您一个想法。对于更多的user_id,您需要先过滤掉它们,对它们单独应用上述操作,然后在最后进行连接。

      【讨论】:

        猜你喜欢
        • 2016-08-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多