<jupyter_start><jupyter_text>Helper Functions<jupyter_code>def prefix_generator(size):
    """
    Helper function to generate a random phone prefix from a set of digits
    """
    return ''.join(random.choices(string.digits, k=size))


# Testing
display(prefix_generator(1))
display(prefix_generator(3))
display(prefix_generator(5))
display(prefix_generator(7))
def price_list_generator(max_size, max_prefix_size, min_price=0.1, max_price=3.0):
    """
    Helper function to generate a price list with :
        - random size : the list size is randomly selected between 1 and `max_size`
        - random prefixes : each prefix has a length randomly selected between 1 and `max_prefix_size`
        - random prices : to each prefix is matched a price from a uniform random distribution [min_price, max_price]
    """
    return sorted([
        [
            prefix_generator(random.randint(1, max_prefix_size)),
            np.round(random.uniform(min_price, max_price), 2)
        ]
        for _ in range(random.randint(1, max_size))
    ])


# Testing
display(price_list_generator(max_size=3, max_prefix_size=5))
display(price_list_generator(max_size=5, max_prefix_size=5))
display(price_list_generator(max_size=7, max_prefix_size=5))
def create_prices_dataset(num_operators, max_size, max_prefix_size=5):
    """
    Helper function for creating a dataset holding the operators, the prefixes for
    each operator, and the price for each prefix
    """
    
    # Since operator names are unknown, we create a list of N letters ['A', 'B', ...]
    operators = list(string.ascii_uppercase)[:num_operators]
    
    # Create a DataFrame holding the operator name, the prefixes for that operator, and the price for each prefix
    df_list = []
    
    for op in operators:
        df = pd.DataFrame(price_list_generator(max_size, max_prefix_size), columns=["Prefix", "Price"])
        df["Operator"] = op
        
        df_list.append(df)
        
    # Return a concatenated dataframe
    return pd.concat(df_list)<jupyter_output><empty_output><jupyter_text>Extract Best Prefix Match for each Operator  Helper Functions<jupyter_code>def get_operator_best_match(number, data):
    """
    Helper function to extract the best prefix match
    
    @arg number (string): the number to call
    @arg data (pd.DataFrame): the prices dataset
    
    @return : a pd.DataFrame holding the data (Operator / Prefix that matches best / Price)
    """
    # Get all prefixes that match the number to call
    all_prefixes = data[data.apply(lambda x: number.startswith(x["Prefix"]), axis=1)]
    
    # Find the longest prefix for each operator
    result = {"Operator": [], "Prefix": [], "Price": []}
    
    for op, group in all_prefixes.groupby("Operator"):
        result["Operator"].append(op)
        result["Prefix"].append(group.iloc[-1]["Prefix"])
        result["Price"].append(group.iloc[-1]["Price"])
        
    return pd.DataFrame.from_dict(result)
def cheapest_operator(price_per_operator):
    """
    Helper function to find the cheapest operator for a given number
    
    @arg price_per_operator (pd.DataFrame): a dictionary holding the data (Operator/Prefix that matches best/Price)
    
    @return: the operator name
    """
    
    return price_per_operator.iloc[price_per_operator["Price"].idxmin()]["Operator"]<jupyter_output><empty_output><jupyter_text>Unit Test<jupyter_code>test_df = pd.DataFrame.from_dict({
    "Prefix"   : ["1", "12", "123", "12", "123456", "1", "99"],
    "Price"    : [0.4, 2.9, 1.4, 2.3, 1.6, 1.3, 2.1],
    "Operator" : ["A", "A", "A", "B", "B", "C", "C"]
})

display(test_df)

num = "123456789"
match_df = get_operator_best_match(num, test_df)

display(HTML("<h3>We can see that the function behaves correctly by returning the expected " \
             "values, which are '123' for 'A' and '123456' for 'B' and '1' for 'C'"))
display(match_df)

display(HTML("<h3>Furthermore, the assertion that the cheapest operator is 'C' is true."))
display(HTML("<h6>cheapest_operator(match_df) == 'C'"))

assert(cheapest_operator(match_df) == "C")<jupyter_output><empty_output><jupyter_text>Testing on a Large DataSet  Generating random numbers to call<jupyter_code>CALL_SIZE = 10     # How many numbers to call
NUM_SIZE  = 10     # The size of each number (e.g. we'd want to call 0123456789, which has a size of 10 digits)

# The function `prefix_generator` comes in very handy to generate random numbers to call
call_list = [prefix_generator(NUM_SIZE) for _ in range(CALL_SIZE)]

print("We'll be calling these numbers :")
display(call_list[:5])<jupyter_output>We'll be calling these numbers :<jupyter_text>Generate a random dataset<jupyter_code>MAX_PREFIX_SIZE = 3
NUM_OPERATORS   = 5     # Number of operators
MAX_SIZE        = 100   # Max list size for each operator

price_df = create_prices_dataset(num_operators=NUM_OPERATORS, max_size=MAX_SIZE, max_prefix_size=MAX_PREFIX_SIZE)

# Remove duplicated prefixes
price_df = price_df.drop_duplicates("Prefix")<jupyter_output><empty_output><jupyter_text>Display a sub-dataset for each operator<jupyter_code>for op, prices in price_df.groupby(["Operator"]):
    display(prices.sample(frac=1).head(5).sort_values(by="Prefix"))<jupyter_output><empty_output><jupyter_text>Adding save / load capabilities  Save to CSV format<jupyter_code>price_df.to_csv("price-list.csv", header=True, index=False)<jupyter_output><empty_output><jupyter_text>Load from CSV<jupyter_code>price_df = pd.read_csv("price-list.csv", dtype={"Operator": str, "Prefix": str, "Price": np.float})
for number in call_list:
    display(HTML("<h3># The number to call : {}".format(number)))
    display(HTML("<h4>The best matches by operator :"))
    
    match_df = get_operator_best_match(number, price_df)
    display(match_df)
    
    display(HTML("<h3>The cheapest operator is : {}".format(cheapest_operator(match_df))))<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11874154262516914, 'fraction_numerical': 0.017422192151556156, 'mean_word_length': 4.4, 'pattern_counts': {'":': 6, '<': 38, '<?xml version=': 0, '>': 38, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '8', 'dir': 'jupyter-structured-clean-dedup', 'id': '644500', 'license': 'MIT', 'n_tokens_mistral': 1966, 'n_tokens_neox': 1884, 'n_words': 498, 'path': 'prototyping.ipynb', 'repo_name': 'redouane-dev/cheapest-phone-call', 'size': '5912'}