Forum Discussion
Equitable distribution by shorter distance - Geolocation
Appreciable all
I require your support for the following, I need to assign clients to the closest manager by location, but these assignments should be distributed evenly, if the closest manager is already saturated, the client should be assigned to the next closest one.
I appreciate any suggestions you have ..
This is what I have done so far
This is the manager’s table
Client’s table
With the manager’s table i Grouped By location
To this table I add another column containing the records that belong to each location
To this new table, I add another column using a function that calculates the distances that exist between the client and each possible administrator. It is a ordered table from less to more distance.
Distances’ Table
Having this last table I can assign the closest manager, as it will be in the first record of the table of distances
Finally I expand this last column and remove auxiliary columns
This is how I assign the closest administrator, however, I do not find how to limit it so that it is distributed equally. The maximum number of records each manager can have would be the total number of clients in the location divided by the number of managers there are.
I thank you in advance for all your help
The code:
//The code with the functions
let
// Functions
Calculate_Distance=(Latitude1 as number, Longitude1 as number, Latitude2 as number, Longitude2 as number) =>
let
EarthRadius = 6378.1,
constante = Number.PI / 180,
DifferenceLat = Latitude1 - Latitude2,
DifferenceLon = Longitude1 - Longitude2,
a = Number.Power(Number.Sin(DifferenceLat * constante / 2) , 2) + Number.Cos(Latitude1 * constante) * Number.Cos(Latitude2 * constante) * Number.Power(Number.Sin(DifferenceLon * constante / 2) , 2),
b = 2 * Number.Asin(Number.Sqrt(a)),
FinalDistance = b * EarthRadius
in FinalDistance,
#"Data with Distance" = (Locations as table, DATA as table) as table =>
let
DataWithDistance=Table.AddColumn(DATA,"Distances", each
Table.Sort(
Table.AddColumn(Locations,"Distance", (RECORD)=>
Calculate_Distance(RECORD[latitude], RECORD[longitude],_[latitude],_[longitude])
), {"Distance"} )
)
in DataWithDistance,
// Processing
Managers = Manager,
Locations = Table.Group(Managers, {"Location"}, {{"Count", each Table.RowCount(_), type number}, {"Managers", each _, type table}}),
Records = Table.AddColumn(Locations, "Data",
each Table.SelectRows(Data,(MainTable) => if MainTable[Location]=[Location] then true else false)
,type table),
#"Records with distance" = Table.AddColumn(Records,"Records_Distance", each #"Data with Distance"(_[Managers],_[Data]), type table),
Assigned = Table.AddColumn(#"Records with distance","Assigned", each
Table.AddColumn([Records_Distance],"Manager", each _[Distances][Manager]{0})),
Expand = Table.ExpandTableColumn(Assigned, "Assigned", {"ID", "Manager"}, {"ID", "Manager"}),
Final = Table.RemoveColumns(Expand,{"Count", "Managers", "Data", "Records_Distance"})
in
Final
22 Replies
- Greg_DecklerCommunity Champion
Dihros - Well, if you need a DAX solution, @ me and let me know, you would start here:
https://community.powerbi.com/t5/Quick-Measures-Gallery/Going-the-Distance/m-p/963267#M423
Otherwise, this is clearly the territory of ImkeF , edhans or someone similar.
Also, posting sample data as text in a table will help them tremendously.
- DihrosFrequent Visitor
Greg_Deckler- Thank you very much for your fast response, at the moment I require to be able to make the assignment / distribution in power query .
If I can't find another alternative, I would appreciate your help to make the process in DAX
Greetings
- AnonymousNot applicable
This seems to me an unusual, albeit very interesting, problem for the context.
the link of a pbix file with a draft solution:
there is a function that allows to obtain the manager-client associations for each location, invoking it with the name of the location.per scaricarli, fai click sul seguente link e segui le istruzioni.
I'm not sure if I have interpreted the need correctly.
The idea is this (assuming that customers are more than managers :)):
I group both customers and managers by location;
for each location I cycle through the list of managers and find the closest customer in the list of customers, which I gradually exclude from the list. This is until the customer list is completely empty.
A different possible way would be to scroll through the list of customers and assign each one to the closest manager, excluding from the list of managers those who gradually become saturated.- AnonymousNot applicable
an aesthetic change, to make the control of the end of the cycle less brutal.
- AnonymousNot applicable
I would like to go back to the literal meaning of the specification for what I understand, to share the following observation.
Assigning to the various customers the closest manager still available (not saturated) without any other specification, implies that the result is highly dependent on the order in which the customers are chosen to be assigned to the closest manager.
If all customers are equally privileged and you choose, starting from the first on the list to the last, you could have a situation in which the sum of the distances thus obtained is much greater than the sum of distances that would be obtained if you scrolled the list of customers on the contrary from the last to the first ,for example.
- AnonymousNot applicable
clients
managers
distrClientsByLocation (via ListAccumulate)
let distr=(loc) => let manTabLoc=managers{[location=loc]}[tabLoc], listClients=clients{[location=loc]}[clientsLoc][id], recsCl=Table.ToRecords(clients{[location=loc]}[clientsLoc]), m=Table.RowCount(manTabLoc), r=Number.RoundUp(List.Count(listClients)/m), exManTab=Table.AddIndexColumn(Table.FromRecords(List.Combine(List.Transform(Table.ToRecords(manTabLoc), each List.Repeat({_},r)))),"idx",0,1), lacc= List.Accumulate(recsCl,[cl={},man={},manT=exManTab], (s,c)=> s& [cl=s[cl]&{c}, man=s[man]&{closestManager(c,s[manT])},manT=Table.RemoveMatchingRows(s[manT],{closestManager(c,s[manT])})]) in Table.FromColumns({lacc[cl],lacc[man]}) in distrclosestManager
let closest=(client, managers) => let distances=List.Transform(List.Zip({managers[lat],managers[long]}), each distance(client[lat],client[long],_{0},_{1})) in managers{List.PositionOf(distances, List.Min(distances))} in closestinvoked function for location 1
let Source = distrClientsByLocLA("l1"), #"Expanded Column1" = Table.ExpandRecordColumn(Source, "Column1", {"id"}, {"id"}), #"Expanded Column2" = Table.ExpandRecordColumn(#"Expanded Column1", "Column2", {"man", "location", "lat", "long"}, {"man", "location", "lat", "long"}) in #"Expanded Column2"results in:
PS
I dont't have enough time now to give some important (I think) comment.
I'll come back to it as soon as possible.
- DihrosFrequent Visitor
Thank you so much Anonymous ,
After doing a lot of tests and learning how to use the List.Generetate function, -thank you for your codes, it helped me a lot to understand how does it work - i did the following code
Although, I have tried to improve the algorithm, it only works with few records, otherwise, it takes too much time that is impossible to process.
I also tested your last code, and likewise, it takes a lot of time.
Any suggestions to make it faster? ,
I am not sure if the process of removing records is the one that slows down the process.
BTW I'm reading the data from an Excel file
PS. I share the code of the whole process in case someone helps , You only need to paste it in the advanced query editor
// Manager's Table let Managers = Table.TransformColumnTypes(Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("XdA9CsMwDAXgqxTPqdCvJd+hPUHIEErp1kLvP9RxCEGdZPD34EnzXO7re309vxcqU7l9HtvQBoRSnfv76grC1tzLMp2ak2Y1atgGF3DG4Ehcdr4NI0CyCI3BCdg4FGvy+udDpIYcgUrRmHLC9sQWtIDmnY8+gcBObpp0PXVFQBbEUScIwpyIk/ZdbzsbA1PvO05THfoaqi3pyNqEXA/NpJqbtKSl/5P7waMG9zWXHw==", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [Manager = _t, Location = _t, latitude = _t, longitude = _t]),{{"latitude", type number}, {"longitude", type number}}), // Client's Table DATA = Table.TransformColumnTypes(Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("lZQ5coNAFETvQixTf1/u4BuoFPkAvn/mBkZIowgroQaePt09Pdzvi7IQRy235fv3R3FxWk2YvGNbfSWvkmLeqcvjdl8aT9J54rW4rXjg+Cdpau14UgpJTnhqS1X54DUiuDpPnvc3X+OJBK/ODzllZVqD78pQjqE+RY0m3KXYVGzglVgmycEzpfE83txg9pQTaUJhevLsH3KEUkn15Btxag89ZbAz8VLUgSmDz3aYH3EW8ZbGrN9wq+KpnzmIpWXwzU764bddsmTnaS3hDbcnDzc980QKQJ88GxFMH/GjPU528NslCHkble6hfRXsNCRIHfkzCUHSi/dYyy2Qz7FdvSJOTvcXz0/9l/ite/LG5+pIv1GTwbcSu482MxVZ5j/0o/82+8UEA7DnWbImZyET3vlNPTFPfCgq6Of8LlZFyQbvJPHBp9VxbxtPjqVZv/CsKZ5m/JJj2A1pFRyYF9962e7BX99e3bbX+6p8JUOfr6e/8+1Xx+MQe/B7eXplw8chqcb46hIVnK3HHw==", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [ID = _t, Location = _t, latitude = _t, longitude = _t]),{{"latitude", type number}, {"longitude", type number}}), // Function to calculate the distance Calculate_Distance = (Latitude1 as number, Longitude1 as number, Latitude2 as number, Longitude2 as number) => let EarthRadius = 6378.1, constante = Number.PI / 180, DifferenceLat = Latitude1 - Latitude2, DifferenceLon = Longitude1 - Longitude2, a = Number.Power(Number.Sin(DifferenceLat * constante / 2) , 2) + Number.Cos(Latitude1 * constante) * Number.Cos(Latitude2 * constante) * Number.Power(Number.Sin(DifferenceLon * constante / 2) , 2), b = 2 * Number.Asin(Number.Sqrt(a)), FinalDistance = b * EarthRadius in FinalDistance, //Function Asign Managers Asign = (ManagersP as table, Records as table) => let TotalMan= Table.RowCount(ManagersP), ClientbyMan= Number.RoundUp(Table.RowCount(Records)/TotalMan), TableDistances=Table.Sort(Table.Buffer(Table.ExpandTableColumn(Records,"Distances", {"Manager", "Distance"}, {"Manager", "Distance"})),"Distance"), //Function to get the available manager Next= (TableO,Asigned)=> List.Last(List.Generate(()=> [Continue = 1, TableI=TableO] , each [Continue]=1 , each [ Continue= if Asigned{List.PositionOf(ManagersP[Manager], [TableI]{0}[Manager])} < ClientbyMan then 0 else 1, TableI= if Continue = 1 then Table.RemoveRows([TableI],0) else [TableI] ] , each [TableI] )), // Loop the table asigning the closest manager Final=List.Generate( ()=> [ Record=Table.First( TableDistances,[ID=null, Manager=null,Distance=null]), Dispo= List.PositionOf(ManagersP[Manager],Record[Manager]), TableSort= Table.RemoveMatchingRows(TableDistances,{[ID=Record[ID]]},"ID"), Counter= List.ReplaceRange(List.Repeat({0},Table.RowCount(ManagersP)),Dispo, 1,{1}), RecordOut=[ Id=Record[ID], Manager=Record[Manager], Distance=Record[Distance] ], Continue= if Table.RowCount( TableSort)>0 then true else false ] , each [Continue] , each [ Record=Table.First( Next([TableSort],[Counter]),[Manager=null] ), Dispo=List.PositionOf(ManagersP[Manager],Record[Manager]), TableSort= Table.RemoveMatchingRows([TableSort],{[ID=Record[ID]]},"ID"), Counter= List.ReplaceRange([Counter],Dispo, 1,{[Counter]{Dispo}+1}), RecordOut = [ Id=Record[ID],Manager=Record[Manager], Distance=Record[Distance] ], Continue= if Table.RowCount( [TableSort])>0 then true else false ] , each [RecordOut] ) in Table.FromList(Final,Record.FieldValues,{"Id","Manager","Distance"}), //Funcion to add a table with the calculate distances #"Data with Distance" = (Locations as table, DATA as table) as table => let DataWithDistance=Table.AddColumn(DATA,"Distances", each Table.Sort( Table.AddColumn(Locations,"Distance", (RECORD)=> Calculate_Distance(RECORD[latitude],RECORD[longitude],_[latitude],_[longitude]) ), {"Distance"} ) ) in DataWithDistance, // Final Process Locations = Table.Group(Managers, {"Location"}, {{"Count", each Table.RowCount(_), type number}, {"Managers", each _, type table}}), Records = Table.AddColumn(Locations, "Data", each Table.SelectRows(DATA,(MainTable) => if MainTable[Location]=[Location] then true else false),type table), #"Records with distance" = Table.AddColumn(Records,"Records_Distance", each #"Data with Distance"(_[Managers],_[Data]), type table), AsignarProcess = Table.AddColumn(#"Records with distance","Final Asign", each Asign(_[Managers],_[Records_Distance])), #"Remove Columns" = Table.RemoveColumns(AsignarProcess,{"Location", "Count", "Managers", "Data", "Records_Distance"}), Final = Table.ExpandTableColumn(#"Remove Columns", "Final Asign", {"Id", "Manager", "Distance"}, {"Id", "Manager", "Distance"}) in Final- AnonymousNot applicable
to deepen the performance aspects (certainly the use of list.accumulate is not highly recommended), it is necessary to know:
the number of locations?
for each location how many managers? 10 to 15 managers, for example
for each location how many customers? 100 to 250 customers, for examplePS
What data did you test the code on and how long did it take?
what is a time you reasonably expect?