Sunday, March 26, 2023

Uber Eats' Swapped Order Problem

While at a restaurant recently, I noticed they had multiple shelves of pickup orders. It's a popular local restaurant, so they had orders from all the major food delivery companies, Doordash, Uber Eats, Grubhub, and they all had little receipts on them.

Each receipt had one line printed really large, and it was different for each of these companies. Here, we'll focus on Uber Eats because it was the most problematic one. For Uber Eats, it says "UBER_EATS #F35DF NO Utensils Courier-powered Delivery - Samuel M (#1423)".

What's the problem?

Firstly, let's go over the problems. There are two numbers here, both seem to be unique to this order. It also puts the originator, the originator's "ID", whether they want utensils or not, and that it has an originator, as well as the orderer's name all at the same level of importance.

Now, one might think these are all very important. But if you actually stand in that restaurant for more than a couple minutes, you'll notice something. All of the delivery drivers follow the same efficient pattern. They come in, read from their app "Uber eats 5DF", then the restaurant worker will go back to the rack, read the receipts in a random order, grab the first one that matches, hand that to the driver, and the driver will run off in a hurry. Notice the driver doesn't double check the receipt. None of them did.

And I don't blame them, what are the chances that "5DF" is repeated? 163, which is "only" 4,096. That means the chances are 0.02%. Note: I used 16 as the base, not 36, because they're only used hexadecimal (0-9 and A-F), which will come up later.

If we were talking about 1 restaurant, or even 100, that would mean that 1 in 4,096 orders (or 1 every 40 days) are getting swapped due to this efficiency. However, we're talking about Uber Eats, which does at least millions of orders a month, which is over 33 thousand a day (on average), or about 10 swaps a day.

This is all assuming drivers pronounce and restaurant workers hear every letter correctly, and no other mistake is made in the selection of an order. If you take that into account, you'll likely see a much higher rate of swaps.

Why does it matter?

I just explained the scope of the problem, but at Uber Eats' scale, 10 swaps a day isn't even an hour of a customer support person's time.

However, if you poll everyone you know that uses these delivery services, there's a consistent theme. If this was truly so rare, why does everyone have a story? As someone who's used these services only in rare circumstances (under 10 times, though I never thought to count), I have 2 such cases. I might be a statistical outlier though.

Given the theoretical scale is small, the cost is likely small, why bother? Because it affects the perception of your service. Perception of a service costs much more than the immediate customer support bill, it affects the business through the customer-acquisition cost (the famous "free" word of mouth), the customer retention rate, and generally the bottom-line.

Okay, so what's the solution?

You might think I've written this much so far and I'm about to write a complicated solution out. Or that it might cost Uber/Uber Eats days or weeks of engineering to write a new receipt format, or require new printers.

No, the reason I believe in this solution is that it's simple. If the drivers are going to read out 3 letters, then give them more options. Make the math use bigger numbers.

Instead of hexadecimal, use all the distinctly-named letters and numbers. Throw out C, D, E since they all sound like B, keeping only A, B, F from the first version. Then add in the other visually- and phonetically-distinct letters: H, I, L, M, Q, R, U, W, X, Y. Now we're up to 23 letters, so the probability is now 1 in 233, or 12,167 or 0.008%, a 3x improvement.

This solution makes no changes to the format of the receipt whatsoever, so it's just a simple transform. Here's some sample code in Python and then in Go, since I know most of Uber Eats is written in Go.

def hex_to_baseN(hex_string, baseN_chars):
    # Convert the hexadecimal string to an integer
    hex_int = int(hex_string, 16)
    new_base = len(baseN_chars)

    # Convert the integer to the new base by repeated division
    baseN_list = []
    while hex_int > 0:
        hex_int, remainder = divmod(hex_int, new_base)
        baseN_list.append(baseN_chars[remainder])

    # Reverse the list and concatenate the characters to form the final string
    baseN_string = ''.join(baseN_list[::-1])

    return baseN_string

def hex_to_base23(hex_string):
    # Define a list of characters to use for base 23
    base23_chars = 'ABFHILMQRUWXY0123456789'
    return hex_to_baseN(hex_string, base23_chars)

And in Go (written with lots of help from ChatGPT and go.dev/play/, since I don't actually have much experience with Go):

import "fmt"

func hexToBaseN(hexString string) string {
    // Define a slice of characters to use for base 23
    base23Chars := []rune("ABFHILMQRUWXY0123456789")
    return hexToBaseN(hexString, base23Chars)
}

func hexToBaseN(hexString string, baseNChars []rune) string {
    // Convert the hexadecimal string to a uint64
    var hexUint64 uint64
    fmt.Sscanf(hexString, "%x", &hexUint64)

    // Convert the uint64 to the new base by repeated division
    var baseN uint64 = uint64(len(baseNChars))
    var baseNRunes []rune
    for hexUint64 > 0 {
        remainder := hexUint64 % baseN
        hexUint64 = hexUint64 / baseN
        baseNRunes = append(baseNRunes, baseNChars[remainder])
    }

    // Reverse the slice and concatenate the runes to form the final string
    baseNString := string(reverseRunes(baseNRunes))

    return baseNString
}

// reverseRunes returns a new slice with the runes in reverse order
func reverseRunes(runes []rune) []rune {
    reversedRunes := make([]rune, len(runes))
    for i, j := 0, len(runes)-1; i <= j; i, j = i+1, j-1 {
        reversedRunes[i], reversedRunes[j] = runes[j], runes[i]
    }
    return reversedRunes
}

Presumably, Uber Eats already has a reverseRunes function, so they don't even need that. That leaves under 30 lines of code, and an extra function call injected right before that line is generated for the receipt printer.

This would take a couple hours of total engineering time, from creating the change (copy-pasting it), getting it past code review, to merging and deploying it. Once in, it would save Uber hundreds of dollars a day in customer support work, refunded bills, and generally reduce food/fuel/time waste.

But, can we do better?

Of course! The above solution is incredibly myopic and assumes the receipt line format was carefully considered outside of the crucial 5 letters focused on above.

Just change the format from "UBER_EATS #F35DF NO Utensils Courier-powered Delivery - Samuel M (#1423)" to something understandable by the driver, restaurant worker, kitchen workers, etc. As in, the people that actually interact with the receipt.

Why UBER_EATS

Does Uber plan on creating a second food delivery platform? Probably not, so let's cut it down to just Uber. That's what the drivers say anyway, because they understand there's no need to say Eats.

#F35DF - aka Order ID

We'll talk about this last.

NO Utensils

Without insight as to why this was added, I can't say whether it's good or not. Maybe Uber Eats had an issue with restaurants adding/omitting utensils so they used this line to emphasize it?

Either way, we can keep this in.

Courier-powered Delivery

That's a lot of big words to mean "someone other than Samuel M will pick this up." An initial thought would be to just put the driver's name on the receipt instead, and leave this section out entirely. However, it's possible that they want to reserve the ability to change the assigned driver at any point.

One might thing we have to leave this in, but there's a secret. There's only two reasons a receipt should start with "Uber", it's an Uber Eats delivery or pickup order; the restaurant is never going to handle delivery for an order through Uber Eats. Though let's say Uber Eats wants to reserve the ability to handle that, too, so there's 3 options: * Restaurant-handled delivery * "Courier-powered delivery" * Pickup order

Let's map that to 3 shorter phrases that everyone understands: * "Eatery delivery" * "Uber delivery" * "Pickup"

Customer's name

This section is only useful for pickup orders, so it should only exist for pickups. In fact, if it's a pickup, customers only say their own name, they never use the order ID, so let's swap this to go earlier so it's at the top of the receipt rather than an arbitrary distance down.

What we're left with:

For pickup orders, what matters most is that it's through Uber Eats and the customer's name.

Before: UBER_EATS #F35DF NO Utensils Pickup - Samuel M (#1423)

After: Uber - Samuel M - NO Utensils - pickup - #F35DF

And for "courier-powered delivery" orders, it's even shorter:

Before: UBER_EATS #F35DF NO Utensils Courier-powered Delivery - Samuel M (#1423) After: Uber - #F35DF - NO Utensils - Uber Delivery

Let's use that space

With all that space, let's throw some word lists at it! We have a few options, from PGP's "biometric word list" to BIP-39's seed phrase word list. I'm going to go with BIP-39 since it's the larger word list, but you can literally pick any list or create your own. Just use 256 words or more to easily beat the previous hexadecimal-based mechanism.

If you use a word list of 2,048 words, and just put two words into the top of the receipt, you get 20482 or 4 million, which leaves you with a chance of 0.00002%. That's a 344x improvement over our simple solution, or a 1,024x improvement over Uber's current solution. That would effectively make order swaps in restaurants happen once a month.

To clarify, since the 5 digits in the ID constitute 5*4=20 bits of information, only 3 digits are used for 12 bits of information. A single word from that word list is 11 bits of information, so a second word brings us to 22 bits, or more information than is currently on the receipt. You could do this by just setting the top 2 bits to "00", but I have some "secret" knowledge that the "order ID" is actually the last 5 digits of the order's UUID, which is a much longer/larger number. That means, those other 2 bits are available when this ID is being generated.

Broader solution:

Before: UBER_EATS #F35DF NO Utensils Courier-powered Delivery - Samuel M (#1423) After: Uber - abstract century - NO Utensils - Uber Delivery

It's still shorter, but now can be easily shouted over a crowded pickup area and heard distinctly.

I won't post the code for this, but it's effectively the same as the above code but with a word list instead of a single string. I guess I'll leave it as an exercise for the Uber Eats engineer reader.

Why isn't this already done?

I hope it's clear that I don't think I'm smarter than anyone at Uber, Uber Eats, or really anywhere. I'm sure multiple people have thought of this or better solutions, and some have even tried implementing it. This isn't a critique of the employees, rather a suggestion that, if it does anything at all, help align the right people to act.

Or, more likely, they all have better things to do than listen to a rando online.



from Hacker News https://ift.tt/ABtjVli

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.