Forum Discussion

AlB's avatar
AlB
Community Champion
5 years ago
Solved

Optimizing PERCENTILEX

Hi all,

Sample pbix attached.

We have a very simplified one-table model (see Table1 below) and and two simple measures:

 

Total revenue = SUM(Table1[Revenue])
PercentileM = 
PERCENTILEX.INC(ALL(Table1[Client_code]), [Total revenue], .25)

 

We place Client_code and [PercentileM] in a table visual. Note the definition of Table1 in the file.  The number of clients can be changed in the code, VAR numClients_

If the number of clients is 50K, the visual takes about 10 seconds to update. If it's 100K it goes up to almost 40 seconds. If it's 1M, it explodes, takes over an hour and finally crashes.

2 questions

1. Is there a way to optimize this, avoid it taking so long for large N? See Page 1 in the attached file

2. (See page 2 in the attached file) Since the value provided by [PercentileM] is actually the same for each row, we're being incredibly inefficient by having it run for each of the N rows in the visual. In fact, the result we're after is what we get if we place [PercentileM] in a card visual, which is very fast. Is there a way to get the result from the card visual and place it in each row of the table visual? So that [PercentileM] is executed only once? To clarify what we mean,  we could create a one-row, one-column calculated table MeasureResult with that result:

 

MeasureResult = {[PercentileM]}

 

and then a measure that just reads that static result to be used in the table visual. See this on page 2 of the attached file

 

ReadFromTable = MAX(MeasureResult[Value])

 

This is however an inflexible solution, since the calculated table is static and the user couldn't apply filters for instance. 

Many thanks

 

MFelix    Zubair_Muhammad    mahoneypat TomMartens  GilbertQ  camargos88  OwenAuger 

 

Table1

Client_code Revenue
1 1
1 2
1 3
1 4
2 1
2 2
2 3
2 4
3 1
3 2
3 3
3 4
N 1
N 2
N 3

N

4

 

  • AlB  Here's an approach that gets you close, and might be further refined to get it just right.  I added a random number to your original table to test it out (so not the totals were the same for all clients) like this

     

    Table1 =
    VAR numClients_ = 1000000 //Change here the number of clients
    VAR clients5rows =
    SELECTCOLUMNS (
    CROSSJOIN (
    SELECTCOLUMNS ( GENERATESERIES ( 1, numClients_ ), "Client_code", [Value] ),
    GENERATESERIES ( 1, 5 )
    ),
    "Client_code", [Client_code],
    "Revenue", CONVERT([Value], DOUBLE)
    )
    var withrandom = ADDCOLUMNS(clients5rows, "number", RANDBETWEEN(1,10000))
    return withrandom
     
    Then made a measure that creates the virtual table, finds the rank, uses that to find the rank at 25% based on # of rows/clients, and then filters down to the sum on that row (or close to it). You could refine it to find the values above and below that level and interpolate like PERCENTILEX does.  For 1M clients, this ran in 3.5 s on my machine.
     
    Percentile M3 =
    VAR vSummary =
        ADDCOLUMNS (
            ALL ( Table1[Client_code] ),
            "cSum",
                CALCULATE (
                    SUM ( Table1[number] )
                )
        )
    VAR vAddRank =
        ADDCOLUMNS (
            vSummary,
            "cRank",
                VAR vThisValue = [cSum]
                RETURN
                    RANKX (
                        vSummary,
                        [cSum],
                        vThisValue,
                        ASC
                    )
        )
    VAR v25rank =
        0.25
            COUNTROWS ( vSummary )
    RETURN
        MINX (
            FILTER (
                vAddRank,
                [cRank] >= v25rank
            ),
            [cSum]
        )
     
    Regards,
    Pat
     
  • TomMartens 

    Thanks. I look forward to your response

    mahoneypat 

    That's great! Thanks very much. The improvement is massive. I thought of something like that initially but assumed (wrongly, clearly) that the built-in implementation of PERCENTILEX would already be (quasi)optimized. I am really, really surprised that its performance is so poor compared to your solution 🤔

    I made a couple of changes to account for the interpolation part. Not difficult but it took me a while to find online a clear definition of the exact algorithm EXCEL/DAX use. This is a first version:

     

    PercentileINC M3 (PatMahoney's) V3 = //Added interpolation 
    VAR wantedPerc_ = [Wanted_perc]
    VAR baseT_ = ALL(Table1[Client_code])
    VAR vSummary = ADDCOLUMNS ( baseT_, "cSum", CALCULATE ( SUM ( Table1[number] ) ) )
    VAR vAddRank = ADDCOLUMNS ( vSummary, "cRank", RANKX (vSummary, [cSum],,ASC,Skip))
    VAR percentileRank_ = (wantedPerc_ * (COUNTROWS(baseT_) - 1)) + 1 //Difference with .EXC
    VAR percRankFloor_ = FLOOR(percentileRank_,1)
    VAR percRankCeiling_ = CEILING(percentileRank_,1)
    VAR ranksT_ = TOPN(percRankCeiling_-percRankFloor_+1, FILTER(vAddRank, [cRank]<=percRankCeiling_), [cRank], DESC) 
    VAR val1_ = MINX(ranksT_,[cSum])
    VAR val2_ = MAXX(ranksT_,[cSum])
    VAR interpolated_ = val1_ + ((val2_ - val1_) * (percentileRank_ - percRankFloor_))
    RETURN
    interpolated_

     

    Runs approx. in the same time as your version; so adding the interpolation has hardly any impact

    And another version, some 20% faster than the previous one:

     

    PercentileINC M3 (PatMahoney's) V4 = //Can it run faster than V3??
    VAR wantedPerc_ = [Wanted_perc]
    VAR baseT_ = ALL(Table1[Client_code])
    VAR vAddRank =    ADDCOLUMNS ( baseT_, "cRank", RANKX (baseT_, CALCULATE (SUM ( Table1[number] )),,ASC,Skip))
    VAR percentileRank_ = (wantedPerc_ * (COUNTROWS(baseT_) - 1)) + 1 //Difference with .EXC
    VAR percRankFloor_ = FLOOR(percentileRank_,1)
    VAR percRankCeiling_ = CEILING(percentileRank_,1)
    VAR valsT_ = TOPN(percRankCeiling_-percRankFloor_+1, FILTER(vAddRank, [cRank]<=percRankCeiling_), [cRank], DESC) 
    VAR auxT_ = ADDCOLUMNS(valsT_, "@Value", CALCULATE(SUM(Table1[number]))) 
    VAR val1_ = MINX(auxT_,[@Value]) 
    VAR val2_ = MAXX(auxT_,[@Value])
    VAR interpolated_ = val1_ + ((val2_ - val1_) * (percentileRank_ - percRankFloor_))
    RETURN
    interpolated_

     

    I will definitely mark your response as solution but want to leave this still open for discussion on question 2.

    By the way, do you know if it's possible in DAX Studio to execute, for instance,  a measure several times and get the average of the execution time? I.e., instead of executing the query manually 10 times, check the timings and extract the average manually, have DAX Studio do it automatically. I believe I'd read somewhere it was possible but cannot find where. Thanks

     

    Please mark the question solved when done and consider giving a thumbs up if posts are helpful.

    Contact me privately for support with any larger-scale BI needs, tutoring, etc.

    Cheers 

     

     

  • Hey AlB , Hey mahoneypat ,

     

    we are facing two problems

    • the lack of ordered data structures (needed for PERCENTILE... RANKX, and many more things)
    • a measure will be evaluated inside a filter context (1M rows, 1M filter contexts). This is a ruling principle, for this, this can not be changed.

    To overcome the first issue I started using a PATH object. My measure contains this line:

     

    var p = CONCATENATEX( ALLSELECTED( 'Table1'[Client_code] ) , [Total revenue] , "|" , [Total revenue] , ASC )

     

     p contains an ordered structure that can be used with all the PATH functions, like PATHLENGTH or PATHITEM.

    The complete measure looks like this:

     

    percentile with path and Client_code = 
    var __Percentile = 0.25
    var p = CONCATENATEX( ALLSELECTED( 'Client_code'[Client_code] ) , [Total revenue] , "|" , [Total revenue] , ASC )
    var position = PATHLENGTH( p ) * __Percentile
    var valueAtPosition = VALUE( PATHITEM( p , position , TEXT ) )
    return
    valueAtPosition

     

    As you can see I'm also using a dedicated Client_code table. I recommend using a dedicated table. There is more than one row per client, for this reason the measure will benefit from filter propagation instead of permanently scanning the table.

    The above measure excutes in ~3.2s on my machine (Intel i7-9750H). I consider this not a substantial gain over the solution mahoneypat provided meaning, maybe we have to face the fact that we already reached the optimum 🙂

     

    In regards to your 2nd question, maybe there is the possibility to create a table that just contains a single row using GROUPBY or SUMMARIZE. Then this table will calculated only once during data refresh. You can use a more simple measure to blend the PERCENTILE value into your fact table.

     

    Stay safe, stay healthy, have data (and of course A Merry Christmas)

    Tom

12 Replies