Forum Discussion

Anonymous's avatar
Anonymous
Not applicable
3 years ago
Solved

How to do custom rounding in DAX?

Hi all,

 

Here is a Christmas brain cracker for our DAX specialists!

I want to create a DAX measure that calculates the percentage of values per category in a table to the total of that table, with the following custom rounding:

- All percentages need to be rounded down to the nearest integer.

- Then it needs to be identified how much percentage points are missing to reach the 100% in total.

- Next, these percentage points need to be added each to the percentages with the largest decimal part in descending order.

Please note the dataset is huge so the impact of the DAX calculation should be limited as much as possible

 

So for example:

Category A: 26.98%,
Category B: 34.78%,
Category C: 38.24%

 

Rounded down this becomes:

Category A: 26
Category B: 34
Category C: 38
26 + 34 + 38 = 98 -> 2 missing percentage points

 

These missing percentage points need to be assigned to the 2 percentages with the highest decimal part:
Category A: 27
Category B: 35
Category C: 38
27 + 35 + 38 = 100%

 

How can I do this with DAX? I think some iteration and /or buffering is required, but how do I do this?

parry2k maybe you know?

  • Anonymous I have modified things to be more in line with your data and requirements. See if this works. Updated PBIX is attached below signature.

    Measure 2 = 
        VAR __Cat2 = MAX('Table'[Category2])
        VAR __Table = 
            GENERATE(
                SUMMARIZE(
                    FILTER(ALLSELECTED('Table'), [Category2] = __Cat2),
                    'Table'[Category1],'Table'[Category2],"Value",MAX([Percentage])),
                    VAR __Value = [Value]
                    VAR __RD = ROUNDDOWN(__Value,0)
                    VAR __Decimal = __Value - __RD
                RETURN
                    ROW(
                        "RD", __RD,
                        "Decimal", __Decimal
                    )
            )
        VAR __MaxDecimal = MAXX(__Table,[Decimal])
        VAR __MaxCategory = MAXX(FILTER(__Table, [Decimal] = __MaxDecimal),[Category1])
        VAR __2ndMaxDecimal = MAXX(FILTER(__Table, [Category1] <> __MaxCategory), [Decimal])
        VAR __2ndMaxCategory = MAXX(FILTER(__Table, [Category1] <> __MaxCategory && [Decimal] = __2ndMaxDecimal),[Category1])
        VAR __Category = MAX('Table'[Category1])
        VAR __Result = 
            IF(
                __Category = __MaxCategory || __Category = __2ndMaxCategory, 
                ROUNDUP(MAX([Percentage]),0), 
                ROUNDDOWN(MAX([Percentage]),0)
            )
    RETURN
        __Result //CONCATENATEX(__Table, [Category1]&":"&[Category2]&":"&[Value]&":"&[RD]&":"&[Decimal],UNICHAR(10)&UNICHAR(13))

     

  • Anonymous Easy fix for that, see below and attached PBIX.

    Measure 2 = 
        VAR __Cat2 = MAX('Table'[Category2])
        VAR __Table = 
            GENERATE(
                SUMMARIZE(
                    FILTER(ALLSELECTED('Table'), [Category2] = __Cat2),
                    'Table'[Category1],'Table'[Category2],"Value",MAX([Percentage])),
                    VAR __Value = [Value]
                    VAR __RD = ROUNDDOWN(__Value,0)
                    VAR __Decimal = __Value - __RD
                RETURN
                    ROW(
                        "RD", __RD,
                        "Decimal", __Decimal
                    )
            )
        VAR __MaxDecimal = MAXX(__Table,[Decimal])
        VAR __MaxCategory = MAXX(FILTER(__Table, [Decimal] = __MaxDecimal),[Category1])
        VAR __2ndMaxDecimal = MAXX(FILTER(__Table, [Category1] <> __MaxCategory), [Decimal])
        VAR __2ndMaxCategory = MAXX(FILTER(__Table, [Category1] <> __MaxCategory && [Decimal] = __2ndMaxDecimal),[Category1])
        VAR __Category = MAX('Table'[Category1])
        VAR __SumDown = SUMX(__Table, [RD])
        VAR __Result = 
            SWITCH(__SumDown,
                99, 
                    IF(
                        __Category = __MaxCategory, 
                        ROUNDUP(MAX([Percentage]),0), 
                        ROUNDDOWN(MAX([Percentage]),0)
                    ),
                98, 
                    IF(
                        __Category = __MaxCategory || __Category = __2ndMaxCategory, 
                        ROUNDUP(MAX([Percentage]),0), 
                        ROUNDDOWN(MAX([Percentage]),0)
                    )
            )
    RETURN
        __Result //CONCATENATEX(__Table, [Category1]&":"&[Category2]&":"&[Value]&":"&[RD]&":"&[Decimal],UNICHAR(10)&UNICHAR(13))
  • Anonymous's avatar
    Anonymous
    3 years ago

    Great, that worked! Thanks so much, very much appreciated 🙂

    And I added an alternative for SWITCH in case nothing needs to be rounded (in the unlikely case all are numbers with 0 decimals)

30 Replies

  • Anonymous Interesting question. Does the remainder always get equally distributed to the top two contributors? 

    • Anonymous's avatar
      Anonymous
      Not applicable

      parry2k the remainder needs to be distributed in descending order. So if the remainder is 1, it is assigned to the percentage that had largest decimal part, and  if the remainder is 2, it is assigned to the two top percentages that had largest decimal part

  • Anonymous you already have an excellent solution from Greg_Deckler I wanted to try new WINDOW functions and see if that will help. Keep in mind, it is based on that you have a category dimension table that has a relationship with the transaction table.

    here is the DAX measure, some of the steps can be collapsed into one step but I just added multiple variables for clarity and to explain the logic behind the solution. ( I will post a collapsed version soon)

    2 - Final Share % = 
    //select current visible category group
    VAR __currentCategory2 = SELECTEDVALUE ( Category[Category2] )
    //create base table for the current visible category group
    VAR __baseTable= 
        ADDCOLUMNS ( 
            SUMMARIZE ( 
                FILTER ( 
                    ALL ( Category ), 
                    Category[Category2] = __currentCategory2  
                ), 
                Category[Category2], 
                Category[Category1] 
            ), 
            "@BaseShare", [1 - Base Share %]   --change this measure to the % measure
        )
    //add a column to round down base %
    VAR __roundTable = 
        ADDCOLUMNS ( 
            __baseTable, 
            "@RoundShare", ROUNDDOWN ( [@BaseShare], 2 ) 
        )
    //add a column to difference between base share and round down share %
    VAR __remainderTable = 
        ADDCOLUMNS (
            __roundTable,
             "@RemainderShare", [@BaseShare] - [@RoundShare] 
        )
    //get the count of remainder to be distributed
    VAR __distributionCount = INT ( SUMX ( __remainderTable, [@RemainderShare] * 100 ) )    
    //find out to which categories the remainder will be distributed, in other words, what base % will be rounded upwards
    VAR __distributionTable = 
        SELECTCOLUMNS ( 
            WINDOW ( 
                1, ABS, 
                __distributionCount, ABS, 
                __remainderTable, 
                ORDERBY ( [@RemainderShare], DESC ) 
            ), 
            [Category1], 
            [Category2] 
        )
    //round up the share %
    VAR __roundUp = 
    CALCULATE ( 
        ROUNDUP ( [Sum Value], 0 ), 
        KEEPFILTERS ( 
            TREATAS (  __distributionTable,  Category[Category1], Category[Category2] ) 
        ) 
    )
    //find result, the one which are not rounded up will be rounded down
    RETURN IF ( __roundUp == BLANK (), ROUNDDOWN ( [Sum Value], 0 ), __roundUp )
    


    Also, if interested check out the full playlist on my youtube channel for new WINDOW DAX functions. https://youtube.com/playlist?list=PLiYSIjh4cEx0BDzmo48YIPzw_dIC0Kd95

  • Greg_Deckler's avatar
    Greg_Deckler
    Icon for Community Champion rankCommunity Champion

    Anonymous I did this (see below). PBIX is attached.

    Measure = 
        VAR __Table = 
            GENERATE(
                SUMMARIZE(ALLSELECTED('Table'),'Table'[Category]),
                    VAR __Value = [Percentage]
                    VAR __RD = ROUNDDOWN(__Value,2)
                    VAR __Decimal = __Value - __RD
                RETURN
                    ROW(
                        "Value", __Value,
                        "RD", __RD,
                        "Decimal", __Decimal
                    )
            )
        VAR __MaxDecimal = MAXX(__Table,[Decimal])
        VAR __MaxCategory = MAXX(FILTER(__Table, [Decimal] = __MaxDecimal),[Category])
        VAR __Category = MAX('Table'[Category])
        VAR __Result = IF(__Category = __MaxCategory, ROUNDUP([Percentage],2), ROUNDDOWN([Percentage],2))
    RETURN
        __Result
    • Anonymous's avatar
      Anonymous
      Not applicable

      Greg_Deckler thanks for your calculation. I tested this on my testdata (see below for more elaborate example), but apparently your calculation only works when there is one percent point to be added. If there are two percent points that need to be added, the total is 99%, see also screenshot below. Maybe I could use a for- or while loop, like you described here?

       

  • ppm1's avatar
    ppm1
    Icon for Solution Sage rankSolution Sage

    Here's a measure expression that shows one way to do it. You'll have to watch out for ties in the MOD values, and for when the values add up to >100 (but you can adapt this measure to do that).

    AdjValue =
    VAR vThisCategory =
        MIN ( T1[Category] )
    VAR vThisVal = [AvgVal]
    VAR vThisRD =
        ROUNDDOWN ( vThisVal, 0 )
    VAR vThisMod =
        MOD ( vThisVal, 1 )
    VAR tNew =
        ADDCOLUMNS ( ALL ( T1[Category] ), "cOrigVal", [AvgVal] )
    VAR tRoundMod =
        ADDCOLUMNS (
            tNew,
            "cRD", ROUNDDOWN ( [cOrigVal], 0 ),
            "cMod", MOD ( [cOrigVal], 1 )
        )
    VAR vGapTo100 =
        100 - SUMX ( tRoundMod, [cRD] )
    VAR vModRank =
        RANKX ( SELECTCOLUMNS ( tRoundMod, "cMod2", [cMod] ), [cMod2], vThisMod, DESC )
    VAR vResult =
        IF ( vModRank <= vGapTo100, ROUNDUP ( vThisVal, 0 ), ROUNDDOWN ( vThisVal, 0 ) )
    RETURN
        vResult

     

    Pat

  • Anonymous Ah! so remainder distribution is dynamic. 

    • Anonymous's avatar
      Anonymous
      Not applicable

      yes indeed!

       

  • Anonymous's avatar
    Anonymous
    Not applicable

    Greg_Deckler ppm1 parry2k thanks for your calculations and input! However, I just realized I do'nt have one but two categories, which each can be filtered separately. The percentages of Category2 make up a total of 100%

     

    So my table looks something like this:

    Category1Category2Percentage
    AX43.75
    BX12.50
    CX12.50
    DX31.25
    AY40.38
    BY5.77
    CY23.08
    DY30.77

     

     

    Each Category2 rounded down this becomes:

    Category1Category2Percentage
    AX43
    BX12
    CX12
    DX31
    AY40
    BY5
    CY23
    DY30


    Category2 X -> 2 missing percentage points

    Category2 Y -> 2 missing percentage points

     

    These missing percentage points need to be assigned to the 2 percentages with the highest decimal part. Please note there is a tie in X of Category2 so one percentage point goes to the first. I guess it must be fairly simple to add to the calculations you provided, but I can't manage to get it work... any ideas? Many thanks in advance!

    Category1Category2Rounded percentage
    AX44
    BX13
    CX12
    DX31
    AY40
    BY6
    CY23
    DY31
    • ppm1's avatar
      ppm1
      Icon for Solution Sage rankSolution Sage

      I see you worked a tie into the example data. Please see this updated measure expression that uses RAND to break the tie.

      AdjValue = 
      VAR vThisVal = [AvgVal]
      VAR vThisCategory1 =
          MIN ( T1[Category1] )
      VAR tNew =
          ADDCOLUMNS (
              CALCULATETABLE ( DISTINCT ( T1[Category1] ), REMOVEFILTERS ( T1[Category1] ) ),
              "cOrigVal", [AvgVal]
          )
      VAR tRoundMod =
          ADDCOLUMNS (
              tNew,
              "cRD", ROUNDDOWN ( [cOrigVal], 0 ),
              "cMod", MOD ( [cOrigVal], 1 ),
              "cRand", RAND()/1000
          )
      VAR vThisModRand = SUMX(FILTER(tRoundMod, T1[Category1] = vThisCategory1), [cMod] + [cRand])
      VAR vGapTo100 =
          100 - SUMX ( tRoundMod, [cRD] )
      VAR vModRank =
          RANKX ( tRoundMod, [cMod] + [cRand], vThisModRand, DESC )
      VAR vResult =
          IF ( vModRank <= vGapTo100, ROUNDUP ( vThisVal, 0 ), ROUNDDOWN ( vThisVal, 0 ) )
      RETURN
          vResult

       

      Pat

      • Anonymous's avatar
        Anonymous
        Not applicable

        Hi Pat ppm1 I tested this on my simple test dataset, and noticed the results are correct! However, it seems that the values assigned to the ties (B and C in Category1) are very instable, i.e. when I refresh the report the values change every time, see screenshots below in column TEST. Maybe this has something to do with the results stored in a virtual table?

         

        After refresh:

         

         

    • bolfri's avatar
      bolfri
      Icon for Solution Sage rankSolution Sage

      Hi,

       

      I think you're trying to do it wrong. Do not create a too complex measures that's not nessesery. 😄

       

      In Power Query M:

      Add a Percentage_to_number column which is your oryginal Percentage divided by 100 and make this as a number (with decimal places)

      let
          Source = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("i45WclTSUYoAYhNjPXNTpVidaCUnqIihkZ6pAVjEGUPEBSpibKhnBNEFMicSZI6BnrEF3ByQiKmeuTncGJCAkbGegQXcGJCIsQFYTSwA", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [Category1 = _t, Category2 = _t, Percentage = _t]),
          #"Replaced Value" = Table.ReplaceValue(Source,".",",",Replacer.ReplaceText,{"Percentage"}),
          #"Changed Type" = Table.TransformColumnTypes(#"Replaced Value",{{"Category1", type text}, {"Category2", type text}, {"Percentage", type number}}),
          #"Added Custom" = Table.AddColumn(#"Changed Type", "Percentage_to_number", each [Percentage] / 100),
          #"Changed Type1" = Table.TransformColumnTypes(#"Added Custom",{{"Percentage_to_number", type number}})
      in
          #"Changed Type1"

       

      In DAX:

      Create a Percentage mesure and change the format to Percentage:

      Percentage measure = SUM('Sample'[Percentage_to_number])

      As you can see it's almost the same as previous.

      To get rid of decimal places simply change it to 0.

       

      Final efect:

       

      The numbers are correct and they are calculated by mathematic law 🙂

      • Anonymous's avatar
        Anonymous
        Not applicable

        Many thanks! This would have been a great solution... but look at the results of the Percentage measure of Category X... 44 + 13 + 13 + 31 = 101 !

        The tie (and I think any two values with ,5 decimal in one category) seems to mess up the total... hence the need for custom rounding. Let me know your thoughts on how to resolve this, thanks 🙂

    • Greg_Deckler's avatar
      Greg_Deckler
      Icon for Community Champion rankCommunity Champion

      Anonymous I have modified things to be more in line with your data and requirements. See if this works. Updated PBIX is attached below signature.

      Measure 2 = 
          VAR __Cat2 = MAX('Table'[Category2])
          VAR __Table = 
              GENERATE(
                  SUMMARIZE(
                      FILTER(ALLSELECTED('Table'), [Category2] = __Cat2),
                      'Table'[Category1],'Table'[Category2],"Value",MAX([Percentage])),
                      VAR __Value = [Value]
                      VAR __RD = ROUNDDOWN(__Value,0)
                      VAR __Decimal = __Value - __RD
                  RETURN
                      ROW(
                          "RD", __RD,
                          "Decimal", __Decimal
                      )
              )
          VAR __MaxDecimal = MAXX(__Table,[Decimal])
          VAR __MaxCategory = MAXX(FILTER(__Table, [Decimal] = __MaxDecimal),[Category1])
          VAR __2ndMaxDecimal = MAXX(FILTER(__Table, [Category1] <> __MaxCategory), [Decimal])
          VAR __2ndMaxCategory = MAXX(FILTER(__Table, [Category1] <> __MaxCategory && [Decimal] = __2ndMaxDecimal),[Category1])
          VAR __Category = MAX('Table'[Category1])
          VAR __Result = 
              IF(
                  __Category = __MaxCategory || __Category = __2ndMaxCategory, 
                  ROUNDUP(MAX([Percentage]),0), 
                  ROUNDDOWN(MAX([Percentage]),0)
              )
      RETURN
          __Result //CONCATENATEX(__Table, [Category1]&":"&[Category2]&":"&[Value]&":"&[RD]&":"&[Decimal],UNICHAR(10)&UNICHAR(13))

       

      • Anonymous's avatar
        Anonymous
        Not applicable

        Brilliant, it works, also in my 'real' dataset! Thanks a lot Greg_Deckler , you're a star! 🙂

        And also thanks to all other contributors bolfri ppm1 parry2k , you helped me shape my thoughts.

  • Anonymous collapsed version:

     

    2 - Final Share Short % = 
    //select current visible category group
    VAR __currentCategory2 = SELECTEDVALUE ( Category[Category2] )
    //create base table for the current visible category group
    VAR __baseTable= 
    ADDCOLUMNS (
        ADDCOLUMNS ( 
            SUMMARIZE ( 
                FILTER ( 
                    ALL ( Category ), 
                    Category[Category2] = __currentCategory2  
                ), 
                Category[Category2], 
                Category[Category1] 
            ), 
            "@BaseShare", [1 - Base Share %],   --change this measure to the % measure
            "@RoundShare", ROUNDDOWN ( [1 - Base Share %], 2 ) 
        ),
        "@RemainderShare", [@BaseShare] - [@RoundShare] 
    )
    //get the count of remainder to be distributed
    VAR __distributionCount = INT ( SUMX ( __baseTable, [@RemainderShare] * 100 ) )    
    //find out to which categories the remainder will be distributed, in other words, what base % will be rounded upwards
    VAR __distributionTable = 
        SELECTCOLUMNS ( 
            WINDOW ( 
                1, ABS, 
                __distributionCount, ABS, 
                __baseTable, 
                ORDERBY ( [@RemainderShare], DESC ) 
            ), 
            [Category1], 
            [Category2] 
        )
    //round up the share %
    VAR __roundUp = 
    CALCULATE ( 
        ROUNDUP ( [Sum Value], 0 ), 
        KEEPFILTERS ( 
            TREATAS (  __distributionTable,  Category[Category1], Category[Category2] ) 
        ) 
    )
    //find result, the one which are not rounded up will be rounded down
    RETURN COALESCE ( __roundUp, ROUNDDOWN ( [Sum Value], 0 ) )
    

     

    • Anonymous's avatar
      Anonymous
      Not applicable

      Wow, that looks really amazing. I need to watch your videos about the WINDOW function to understand what you've done, that's going to be my next task on my list. Thanks a lot ! 🙂

    • Anonymous's avatar
      Anonymous
      Not applicable

      parry2k I am trying to reproduce this but I am getting a bit lost... are you able to share the PBIX-file to which this measure belongs? Many thanks!

  • hi Anonymous Sorry, I was traveling and was not able to get to you on the pbix file. Let me know if you still need the file. Cheers!!

    • Anonymous's avatar
      Anonymous
      Not applicable

      parry2k yes I would still like to have a look at it! Thanks ğŸ™‚

    • Anonymous's avatar
      Anonymous
      Not applicable

      parry2k thanks, I will have a look and let you know!