PowerBI中构建日期区间表的终极方法

什么是日期区间表

只有日期表的叫法,参考:日期表,实际上并没有对日期区间表的约定速成的叫法,但日期区间表却是在PowerBI数据建模分析中有非常重要的运用,下面先看一个例子:

可以看出,当选择年份后,这里希望迅速选择对比的粒度是月,周,日其中的一个,并迅速展开对比及可视化观察。所以要求可以直接点击选择来进行,选择周的效果如下:

以及选择日的效果如下:

这里的优势在于:不需要离开纸面,直接选择对比粒度展开对比。这里称可以动态变化的对比粒度为日期区间

这种日期区间表和日期表显然是不同的,它可以这样理解:

如果从左到右,依次可称为:YearMonthPeriod,YearWeekPeriod以及YearDatePeriod,观察它们的PeriodName字段,名称相同但内容不同,所以CalendarPeriod应该是这三个表的纵向追加合并结果,下面给出构造方法。

用M构建日期区间表

可以自行理解,或者不必理解细节,直接复制粘贴用即可。在PowerBI查询编辑中,新建空查询,然后粘贴如下内容:


let

    calendar_period_type =  type function (
        
            optional CalendarYearStart as (type number meta [
                Documentation.FieldCaption = "开始年份,日期区间表从开始年份1月1日起。",
                Documentation.FieldDescription = "日期区间表从开始年份1月1日起",
                Documentation.SampleValues = { Date.Year( DateTime.LocalNow( ) ) - 1 } // Current Year
            ]),
            
            optional CalendarYearEnd as (type number meta [
                Documentation.FieldCaption = "结束年份,日期区间表至结束年份12月31日止。",
                Documentation.FieldDescription = "日期区间表至结束年份12月31日止",
                Documentation.SampleValues = { Date.Year( DateTime.LocalNow( ) ) } // Previous Year
            ]),

            optional CalendarFirstDayOfWeek as (type text meta [
                Documentation.FieldCaption = "定义一周开始日,从 Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday中选择一个,缺省默认为Monday。",
                Documentation.FieldDescription = "从 Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday中选择一个,缺省默认为Monday。",
                Documentation.SampleValues = { "Monday" }
            ]),

            optional CalendarCulture as (type text meta [
                Documentation.FieldCaption = "指定日期区间表显示月以及星期几的名称是中文或英文,en 表示英文,zh 表示中文,缺省默认与系统一致。",
                Documentation.FieldDescription = " en 表示英文,zh 表示中文,缺省默认与系统一致。",
                Documentation.SampleValues = { "zh" }
            ])

        ) 
        as table meta [
            Documentation.Name = "构建日期区间表",
            Documentation.LongDescription = "创建指定年份之间的日期区间表。并可进行各种设置。",
            Documentation.Examples = {
            [
                Description = "返回当前年份日期区间表",
                Code = "CreateCalendarPeriod()",
                Result = "当前年份日期区间表。"
            ],
            [
                Description = "返回指定年份的日期区间表",
                Code = "CreateCalendarPeriod( 2017 )",
                Result = "返回2017/01/01至2017/12/31之间的日期区间表。"
            ],
            [
                Description = "返回起止年份之间的日期区间表",
                Code = "CreateCalendarPeriod( 2015 , 2017 )",
                Result = "返回2015/01/01至2017/12/31之间的日期区间表。"
            ],
            [
                Description = "返回起止年份之间的日期区间表,并指定周二为每周的第一天",
                Code = "CreateCalendarPeriod( 2015 , 2017 , ""Tuesday"" )",
                Result = "2015/01/01至2017/12/31之间的日期区间表,且周二是每周的第一天。"
            ],
            [
                Description = "返回起止年份之间的日期区间表,并指定周二为每周的第一天,并使用英文显示名称。",
                Code = "CreateCalendarPeriod( 2015 , 2017 , ""Tuesday"", ""en"" )",
                Result = "2015/01/01至2017/12/31之间的日期区间表,且周二是每周的第一天,并使用英文显示月名称及星期几的名称。"
            ]
            }
        ],

    f_create_calendar_period = ( 
        optional CalendarYearStart as number, 
        optional CalendarYearEnd as number, 
        optional CalendarFirstDayOfWeek as text, 
        optional  CalendarCulture as text) =>
    
    let

        begin_year = CalendarYearStart ,
        end_year = CalendarYearEnd ,
        first_day_of_week = if Text.Lower( CalendarFirstDayOfWeek ) = "monday" then Day.Monday
                            else if Text.Lower( CalendarFirstDayOfWeek ) = "tuesday" then Day.Tuesday
                            else if Text.Lower( CalendarFirstDayOfWeek ) = "wednesday" then Day.Wednesday
                            else if Text.Lower( CalendarFirstDayOfWeek ) = "thursday" then Day.Thursday
                            else if Text.Lower( CalendarFirstDayOfWeek ) = "friday" then Day.Friday
                            else if Text.Lower( CalendarFirstDayOfWeek ) = "saturday" then Day.Saturday
                            else if Text.Lower( CalendarFirstDayOfWeek ) = "sunday" then Day.Sunday
                            else if CalendarFirstDayOfWeek <> null then error "参数错误:参数CalendarFirstDayOfWeek必须是Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday中的一个。"
                            else Day.Monday ,
        culture = if CalendarCulture <> null then CalendarCulture else null , // "en" , "zh"
        y1 = if begin_year <> null then begin_year else if end_year <> null then end_year else Date.Year( DateTime.LocalNow() ) ,
        y2 = if end_year <> null then end_year else if begin_year <> null then begin_year else Date.Year( DateTime.LocalNow() ) ,
        
        // calendar list is here:
        
        calendar_list = { Number.From ( #date( Number.From( 2016 ) , 1 , 1 ) ) .. Number.From( #date( Number.From( 2017 ) , 12, 31 ) ) },
        calendar_list_table = Table.FromList(calendar_list, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
        #"Changed Type" = Table.TransformColumnTypes( calendar_list_table ,{{"Column1", type date}}),
        #"Renamed Columns" = Table.RenameColumns(#"Changed Type",{{"Column1", "Date"}}),
        #"Inserted Year" = Table.AddColumn(#"Renamed Columns", "Year", each Date.Year([Date]), Int64.Type) ,

        // build the period for year-month

        period_year_month = 
        let
            #"Inserted PeriodType" = Table.AddColumn(#"Inserted Year", "PeriodType", each "Month" , type text),
            #"Inserted Month" = Table.AddColumn(#"Inserted PeriodType", "PeriodNameOrderBy", each Date.Month([Date]), Int64.Type),
            #"Inserted Month Name" = Table.AddColumn(#"Inserted Month", "PeriodName", each Date.MonthName([Date]), type text)
        in
            #"Inserted Month Name",

        // build the period for year-week

        period_year_week = 
        let
            #"Inserted PeriodType" = Table.AddColumn(#"Inserted Year", "PeriodType", each "Week" , type text),
            #"Inserted Week" = Table.AddColumn(#"Inserted PeriodType", "PeriodNameOrderBy", each Date.WeekOfYear([Date]), Int64.Type),
            #"Inserted Week Name" = Table.AddColumn(#"Inserted Week", "PeriodName", each "W" & Text.From( [PeriodNameOrderBy] ) , type text)
        in
            #"Inserted Week Name",

        // build the period for year-date

        period_year_date = 
        let
            #"Inserted PeriodType" = Table.AddColumn(#"Inserted Year", "PeriodType", each "Day" , type text),
            #"Inserted Date" = Table.AddColumn(#"Inserted PeriodType", "PeriodNameOrderBy", each Number.From([Date]) , Int64.Type),
            #"Inserted Date Name" = Table.AddColumn(#"Inserted Date", "PeriodName", each Text.From( [Date] ) , type text)
        in
            #"Inserted Date Name",

        combined_month_week_date_period = Table.Combine( { period_year_month , period_year_week , period_year_date } ),
        #"Inserted PeriodTypeOderby" = Table.AddColumn( combined_month_week_date_period , "PeriodTypeOrderBy", 
            each if [PeriodType] = "Month" then 1 else if [PeriodType] = "Week" then 2 else 3  , Int64.Type )

    in
        #"Inserted PeriodTypeOderby"
in
    Value.ReplaceType( f_create_calendar_period , calendar_period_type )

这种构造方式与日期表如出一辙,保留了灵活性可以应对复杂的需求。

这里的M构造的编写使用Visual Studio Code进行,它对编写M提供了实时的提示和着色,有兴趣自己编写M的伙伴可以尝试,如下:

日期表与日期区间表的联动

细心的话可以发现,日期区间表在每一个子表,前述的YearMonthPeriod,YearWeekPeriod以及YearDatePeriod中都存在同样的日期字段,也就是例如2016/01/01会出现3次,那么就无法使用日趋区间表的日期字段与业务事实表来关联,因为业务事实表也会存在多个相同日期,关联将导致直接多对多关系,这是不允许的。

这里可以使用日期表作为桥表,将日期区间表与业务事实表隔离,如下所示:

除了进行隔离,为了使日期区间表可以对业务事实表起到筛选作用,需要启动PowerBI提供的双向筛选器。如此一来,就可以顺利地完成两类任务:

  1. 使用日期表进行静态日期筛选。
  2. 使用日期区间表进行动态日期区间筛选。

总结

这里给出了日期表的伴侣日期区间表,它们可以在一起完美地配合工作,为从日期维度进行对比分析起到重要支持。如果发现BUG或更好方法,欢迎一起探讨。

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

推荐阅读更多精彩内容