Forum Discussion

Anonymous's avatar
Anonymous
Not applicable
4 years ago
Solved

Scaling a complex query approach to run multiple scenarios?

I have solved a complex evaluation (binomial distribution) in Power Query.  The resulting approach use a number of functions and steps for ranking, cumulative evaluation, etc.  Point being it works, but given its complexity, I don't see an opportunities to simplify it further into a function or similar.

 

The issue I have is that the results of this evaluation are contextual... the individual record results are calculated in context of all the records in the source.

I would like to scale this solution to present results for multiple scenarios.  Each scenario would need to be independently processed and collected.  This design would allow users to select the scenario they are interested in reviewing.

 

I can manually generate these different scenarios by changing the source records being fed into the evaluation.  However, that is not scalable.

I am trying to figure out if there is a way to

  1. Automate parsing my source list, which can start as a collective set of all scenarios, e.g. list of all departments, but generate separate source scenarios for each department
  2. feed each scenario into my logic (i.e., my established Power Query solution)
  3. incrementally output and collect the scenarios for review/reporting

For #1, parsing scenarios, suspect looping logic could work but not confident, given success with that, how to feed to #2.

For #2, evaluation logic, I had the notion to establish this logic as a dataflow and separately feed in the scenario data.

For #3, collecting outputs, I have focused on solving #1 and #2 first, but presume this is not the biggest challenge.

 

I see this as three separate challenges, and welcome input on any/all, even outside of pure PQ/M solutions.  Perhaps this is a Use Case for Python or Power Automate?

 

Thank you!

  • MarkLaf's avatar
    MarkLaf
    4 years ago

    Long response incoming ðŸ˜¬. Given length of explanations, I'm posting a Cacls2 query first with the requested "At Least" Binomial Calculation.

     

    "At Least" Binomial Calculation 

     

    let
        Source = Data,
        GroupDept = 
        Table.Group( 
            Source, 
            {"Department"}, 
            {{"Data", each _, type table [Department=nullable text, Success=logical]}}
        ),
        PerformCalcs = 
        let 
            bfunc = (x as number, n as number, P as number) => 
            // b(x; n, P) = nCx * P^x * (1 – P)^(n – x)
                Number.Combinations( n, x ) * Number.Power(P,x) * Number.Power(1 - P, n - x),
            ScenarioSchema = type table [
                x=Int64.Type,
                Discrete=number,
                Cumulative=number, 
                At Least = number
            ]
        in
        Table.AddColumn(
            GroupDept, "Calcs", 
            each let
                n = Table.RowCount( [Data] ),
                Successes = Table.RowCount( Table.SelectRows( [Data], each [Success] ) ),
                P = Successes / n,
                xList = List.Numbers(0,n + 1),
                Discrete = List.Generate(
                    ()=> 0,
                    each _ <= n,
                    each _ + 1,
                    each bfunc(_,n,P)
                ),
                DiscreteBuffed = List.Buffer(Discrete),
                Cumulative = List.Generate(
                    ()=> 0,
                    each _ <= n,
                    each _ + 1,
                    each List.Sum( List.FirstN( DiscreteBuffed, _ + 1 ) )
                ),
                CumulativeBuffed = List.Buffer(Cumulative),
                AtLeast = List.Generate(
                    ()=> 0,
                    each _ <= n,
                    each _ + 1,
                    each 1 - (CumulativeBuffed{_} - DiscreteBuffed{_})
                ),
                ScenariosTable = #table( ScenarioSchema, List.Zip({xList,Discrete,Cumulative,AtLeast}) )
            in
                [n=n,P=P,Scenarios=ScenariosTable],
            type [n=Int64.Type,P=number,Scenarios=ScenarioSchema]
        ),
        RemoveDataCol = Table.RemoveColumns(PerformCalcs,{"Data"})
    in
        RemoveDataCol

     

    Switch out Calcs with the new Calcs2 above. The Metadata query won't need any changes, but you'll have to redo expand step in Scenarios.

    Can you break down your Calcs logic/approach for me (comments apply to previous post, not above solution, but techniques are the same)

    1) We group on Department so that we have one row per department, and a [Data] column that houses all data per each Department. When we add a custom column without expanding [Data], we are able to reference the whole dataset for each Department with a reference to [Data]

    2) Overall, our add custom column step is creating a record column. We make a few calculations that we put into a record that we can expand in subsequent steps. let/in is used to define sub-outputs for re-use and readability. Simple examples that show these fundamentals:

     

    // Add custom column to simple tabble without using let/in
    Table.AddColumn( 
        //create single column table; col name is [Num] of type Int64; vals are 0-3
        #table( type table [Num=Int64.Type], {{0},{1},{2},{3}}),
        "RecordColumn",
        each [
            TextRec = Text.From([Num]) & " Text!",
            PowerRec = Number.Power( 2, [Num] ),
            StaticRec = 90.01
        ],
        //here we specify the column type; not strictly required but a good practice to include
        type [TextRec=text, PowerRec = Int64.Type, StaticRec = Decimal.Type]
    )
    
    // Add same custom column to table using let/in
    Table.AddColumn( 
        //create single column table; col name is [Num] of type Int64; vals are 0-3
        #table( type table [Num=Int64.Type], {{0},{1},{2},{3}}),
        "RecordColumn",
        each let
            textval = Text.From([Num]) & " Text!",
            powerval = Number.Power( 2, [Num] ),
            staticval = 90.01
        in
            [TextRec = textval, PowerRec = powerval, StaticRec = staticval],
        //here we specify the column type; not strictly required but a good practice to include
        type [TextRec=text, PowerRec = Int64.Type, StaticRec = Decimal.Type]
    )

     

    FYI Equivalent (with let/in) using UI (minus setting the column type, which isn't available in UI):

    3) The record we are returning per row includes [n] and [P], which are similar to the calculations in the above simple examples (except the field we are referencing is a table instead of a number as in the example). However, the value for [Scenarios] is going to be a table of x,b pairings. E.g.:

    Breaking down how we achieve this:

    a) First, it's important to note that any time we want to build out custom data through iteration or recursion, List.Generate is by and far the best option performance-wise, so it is very useful to get familiar with it. ScenariosList's List.Generate in plain English:

    i) while the field [x] <= n...

    ii) start the first list item with a record with field [x] eqal to 0, and field [b] equal to bfunc(0,n,P)

    iii) check against 'while' criteria by adding +1 to latest value of [x] field and calculating next bfunc and assigning to [b] field

    iv) (implicit because we don't specify last 4th argument) because no output is specified, use the value (i.e. our record with x and b fields) that was used to check against criteria

    Quick correction note: I fixed this in my previous post, but please note that the x argument for bfunc in the 3rd argument of List.Generate should be [x]+1, not [x]. I also switched to simpler syntax rather than Record.TransformFields 

    b) Just to double-check, let's look at the actual list getting generated from the internal ScenariosList, which is where most of the work is occurring. Below, we are modifying the Table.AddColumn portion of the PerformCalcs step to output ScenariosList instead of ScenariosTable (recommend duplicating Calcs query and modifying the duplicate):

     

    Table.AddColumn(
        GroupDept, "Calcs", 
        each let
            n = Table.RowCount( [Data] ),
            Successes = Table.RowCount( Table.SelectRows( [Data], each [Success] ) ),
            P = Successes / n,
            ScenariosList = List.Generate(
                ()=> [x=0, b=bfunc(0,n,P)],
                each [x] <= n,
                each [x=[x]+1,b=bfunc([x]+1,n,P)]
            ),
            ScenariosTable = Table.FromRecords(ScenariosList,ScenarioSchema)
        in
            ScenariosList, //OLD CODE: [n=n,P=P,Scenarios=ScenariosTable],
        type list //OLD CODE: type [n=Int64.Type,P=number,Scenarios=ScenarioSchema]
    )

     

    When we inspect the output, we see that we are indeed generating a list of records where each record has an x and b field.

    c) There is a function built specifically to convert a list of records into a table: Table.FromRecords. Some quick examples to show how it works:

     

    let
        Source = 
        {
            [Field1 = "val_1", Field2 = 1],
            [Field1 = "val_2", Field2= null],
            [Field1 = "val_3", Field2 = 3, Field3 = Number.Random()],
            [Field1 = "val_4"],
            [Field1 = "val_5", Field2 = 5]
        },
    
        //All below steps are referenceing Source, not previous step
        Simple = Table.FromRecords( Source ),
        UseNullForMissing = Table.FromRecords( Source, null, MissingField.UseNull ),
        WithFieldNames = Table.FromRecords( Source, {"Field1","Field2"} ),
        WithTableSchema = 
        Table.FromRecords( 
            Source, 
            type table [Field1=text,Field2=Int64.Type] 
        ),
        AllTogether = 
        Table.FromRecords( 
            Source, 
            type table [Field1=nullable text,Field2=nullable Int64.Type], 
            MissingField.UseNull 
        ),
        WithAllFields = 
        Table.FromRecords( 
            Source, 
            type table [
                Field1=nullable text,
                Field2=nullable Int64.Type,
                Field3=nullable Decimal.Type
            ], 
            MissingField.UseNull 
        )
    in
        WithAllFields

     

    So, as shown from the above query if you paste into advanced editor and look at variations of use, our ScenariosTable internal step is converting the list of records created from ScenariosList into a well-typed table in our Scenarios field, which can be expanded in one step (after expanding the record of which Scenarios is a field). We are not using MissingField.UseNull because we are constructing the list and don't expect any missing fields, so would want it to error if somehow a field didn't show. And to be clear, we could alternatively i) output the lists as-is in custom column, ii) expand list column to new rows (now each row is a record), iii) expand the record column to show fields, and finally iv) transform column types. There isn't a performance difference, so it comes down to preference.

16 Replies

  • Here is a shot in the dark. Perhaps some of the below will give you some ideas. Steps and output:

     

    • Data: Table is 26 departments (with letter names, 'A', 'B', 'C', etc.) with random (specifying seed so numbers stay consistent) number of true/false values to simulate binomial data.

     

     

    let
        Source = 
        Table.FromColumns( 
            { 
                List.Generate(
                    ()=> [i=0,o=Character.FromNumber(65)], 
                    each [i] < 26 , 
                    each [i=[i]+1,o=Character.FromNumber(i+65)], 
                    each [o] 
                ) 
            }, 
            type table [Department=text] 
        ),
        AddSucceses = 
        Table.AddColumn(
            Source, 
            "Success", 
            each List.Transform( 
                List.Random( 
                    List.Sum( 
                        List.Transform( 
                            List.Random(10,54321 + Character.ToNumber([Department])*99), 
                            each Int64.From( Number.Mod(_,1)*100 ) 
                        ) 
                    ),
                    12345 + Character.ToNumber([Department])*37
                ), 
                each Logical.From( Number.Round( _ ) ) 
            )
        ),
        ExpandSuccesses = Table.ExpandListColumn(AddSucceses, "Success"),
        Type = Table.TransformColumnTypes(ExpandSuccesses,{{"Success", type logical}})
    in
        Type

     

     

    • Calcs [do not load]: Where most of the work happens. First, group on [Department], then add a calcs column where we use formula b(x; n, P) = nCx * P^x * (1 – P)^(n – x) to calculate binomial probability for each x 0 -> n, where n is number of true/false records per Department. We first calculate n and P, then iterate through every x with List.Generate to calculate the probability. We want to extract two tables from this output, which is why we aren't expanding anything and not loading.

     

     

    let
        Source = Data,
        GroupDept = 
        Table.Group( 
            Source, 
            {"Department"}, 
            {{"Data", each _, type table [Department=nullable text, Success=logical]}}
        ),
        PerformCalcs = 
        // b(x; n, P) = nCx * P^x * (1 – P)^(n – x)
        let 
            bfunc = (x as number, n as number, P as number) => 
                Number.Combinations( n, x ) * Number.Power(P,x) * Number.Power(1 - P, n - x),
            ScenarioSchema = type table [x=Int64.Type,b=number]
        in
        Table.AddColumn(
            GroupDept, "Calcs", 
            each let
                n = Table.RowCount( [Data] ),
                Successes = Table.RowCount( Table.SelectRows( [Data], each [Success] ) ),
                P = Successes / n,
                ScenariosList = List.Generate(
                    ()=> [x=0, b=bfunc(0,n,P)],
                    each [x] <= n,
                    each [x=[x]+1,b=bfunc([x]+1,n,P)]
                ),
                ScenariosTable = Table.FromRecords(ScenariosList,ScenarioSchema)
            in
                [n=n,P=P,Scenarios=ScenariosTable],
            type [n=Int64.Type,P=number,Scenarios=ScenarioSchema]
        ),
        RemoveDataCol = Table.RemoveColumns(PerformCalcs,{"Data"})
    in
        RemoveDataCol

     

     

    • Metadata: Creating this table for modeling purposes and to house n and P for each Department (rather than repeat per row in the other tables.

     

     

    let
        Source = Calcs,
        ExpandCalcsMeta = Table.ExpandRecordColumn(Source, "Calcs", {"n", "P"}, {"n", "P"})
    in
        ExpandCalcsMeta

     

     

    • Scenarios: The main output from our efforts in Calcs. This houses every x from 0 -> n per Department along with the binomial probability.

     

     

    let
        Source = Calcs,
        #"Expanded Calcs" = Table.ExpandRecordColumn(Source, "Calcs", {"Scenarios"}, {"Scenarios"}),
        #"Expanded Scenarios" = Table.ExpandTableColumn(#"Expanded Calcs", "Scenarios", {"x", "b"}, {"x", "b"})
    in
        #"Expanded Scenarios"

     

     

    We can now load the three tables with relationships Data <-M:1-- Metadata --1:M-> Scenarios and do a few drag and drops to get some visuals with distrobutions.

    • Anonymous's avatar
      Anonymous
      Not applicable

      I am very intrigued by your approach, but am having trouble standing it up.

       

      For Calcs, I get an error, "An error occurred in the ‘’ query. Expression.Error: The name 'RemoveDataCol​' wasn't recognized. Make sure it's spelled correctly.", which then cascades through Metadata and Scenarios.

       

      Can you advise how to fix the error so I can look more closely?

       

      • MarkLaf's avatar
        MarkLaf
        Icon for Super User rankSuper User

        Sorry about that. I think the html generated by the code blocks somehow snuck in some 0-width characters or encoding at the end, appending to last line, so the "in QueryStepName" portion was actually "in QueryStepName[and_hidden_characters]", so was throwing an error when it was looking for the QueryStepName[and_hidden_characters] step. I repasted everything and actually tested out, and I believe it should work without issues now if you just copy/paste into advanced editor and use same query names.

         

        If the issue persists, you can just go to advanced editor, copy last step name, then overwrite the step reference at very end after the in.

  • This is a pretty broad question. If the data you're loading is highly variable, then it seems like DirectQuery and dynamic M parameters are likely to be involved. DAX is also very good at dynamic computations and it might make sense to move some of the final steps from M to DAX (use M to gather and filter the data you need and DAX do more of the final aggregation and computations).

     

    Without seeing your input and output and code, it's hard to give more specific advice.

    • Anonymous's avatar
      Anonymous
      Not applicable

      Alexis,

       

      Thank you for the response.

       

      Yes, I thought of DAX, however, judging by my current solution, 1.) refactoring the approach in DAX is beyond my skills 2.) I don't think the result would be performant.

       

      I could cleanse my effort and provide a file, but this is less about the code and more seeking input for where to take my solution design.

      If we accept as a given that one has a query with established logic and output results, the question is how could one dynamically feed different datasets and collect the discrete outputs?

       

      One scenario I am wondering about is transforming my source dataset using Group By and All Rows to generate a table data type for each scenario... however, I am still unclear, if I accomplished that, how to adjust my equation queries to accommodate from there.

       

      Can you point me towards more information about what you were thinking regarding, "DirectQuery and dynamic M parameters"?  At this point, all ideas are worth looking at.

       

      Appreciate the response and opportunity to brainstorm.

       

      Regards

      • AlexisOlson's avatar
        AlexisOlson
        Icon for Super User rankSuper User

        I understand where you're coming from but it's really hard to discuss design for this sort of thing in generalities since some of the details can drastically change what sort of approach is advisable or even feasible. For example, if all of your potential datasets are just various combinations that can be selected from a single data source, this will be solvable much more easily than if your dataset universe is not pre-determined and you have to set up some sort of data ingestion pipeline as part of the project. If your dataset universe is sufficiently bounded, you can also use Import and load all of your data rather than DirectQuery to pull only one set of data at a time (Import allows much greater flexibility in terms of transformations).

         

        So without seeing anything specific, the best I can do is point you at this:
        https://docs.microsoft.com/en-us/power-bi/connect-data/desktop-dynamic-m-query-parameters

         

        It's obviously not as good a cleansed, working .pbix file, but even just showing your M code would help show what kinds of inputs, transformations, and outputs you're dealing with.