import functions.extract_function as get
import functions.preprocessing_function as preprocess
import functions.modelling_function as modelling
import os
import re
import math
import numpy as np
from rapidfuzz import process, fuzz, utils
from simpletransformers.classification import ClassificationModel
from transformers import pipeline
import gradio as gr
# set current directory
os.chdir(os.path.dirname(os.path.abspath(__file__)))
def is_nan(text):
try:
value = float(text)
return math.isnan(value)
except ValueError:
return False
# Function for preparing catalog
def prepare_catalog():
# Load internal catalog
product_catalog = get.internal_data('catalog')
# Load external catalog
registered_fertilizers = get.registered_fertilizer_data()
# Product catalog cleaning
product_catalog = preprocess.clean_dataframe(product_catalog, 'Product SKU', remove_na=False, remove_non_words=True, remove_symbols=True)
product_catalog['Product SKU Full Clean'] = product_catalog['Product SKU Clean'] + ' ' + product_catalog['Brand'].str.lower() + ' ' + product_catalog['Type'].str.lower()
product_catalog['Product SKU Full'] = product_catalog['Product SKU'] + ' ' + product_catalog['Brand'].str.lower() + ' ' + product_catalog['Type'].str.lower()
# Removing Duplicates:
product_catalog = preprocess.fuzzy_join_compare(product_catalog, 'Product SKU Clean', 'Product SKU Full Clean', registered_fertilizers, take_regist_number=True, set_ratio_weight=1, ratio_weight=0)
# 1. Only take registered fertilizers that is NOT in the existing product catalog
registered_fertilizers = preprocess.slice_with_filter(registered_fertilizers, 'Nomor Pendaftaran', product_catalog, use_filter=True, filter_condition= product_catalog['Max Similarity Score'] > 80)
# 2. Combine product catalog and registered fertilizers
combined_catalog = preprocess.combine_catalog(product_catalog['Product SKU Full'], registered_fertilizers['Nama Lengkap'], 'Product Catalog', 'Registered Fertilizers')
# 3. Remove duplicates
combined_catalog = combined_catalog.drop_duplicates()
# Use lambda function to extract the formula from Registered Product column
combined_catalog['Formula'] = combined_catalog['Registered Product'].apply(lambda x: re.findall(r'\d{1,2}\s*[- ]\s*\d{1,2}\s*[- ]\s*\d{1,2}', x))
# if formula is empty list, then replace it with NaN, else take the first item in the formula list
combined_catalog['Formula'] = combined_catalog['Formula'].apply(lambda x: np.nan if len(x) == 0 else x[0])
return combined_catalog
# Your existing decision function
def decision(user_input, type, catalog, product_name_catalog):
# Initialize the model
pipe_detect = pipeline("text-classification", model="matthewfarant/indobert-fertilizer-classifier", token = os.getenv('HF_MY_TOKEN'))
pipe_match = pipeline("text-classification", model="matthewfarant/autotrain-fertilizer-pair-classify", token = os.getenv('HF_MY_TOKEN'))
# Extract formula
user_input_formula = re.findall(r'\d{1,2}\s*[- ]\s*\d{1,2}\s*[- ]\s*\d{1,2}', user_input)
user_input_formula = np.nan if len(user_input_formula) == 0 else user_input_formula[0]
if type == 'Fuzzy Search':
# Similar Product
catalog['Similarity Score'] = catalog[product_name_catalog].apply(lambda x: fuzz.token_set_ratio(user_input, x, processor=utils.default_process))
catalog['Formula Similarity'] = catalog['Formula'].apply(lambda x: fuzz.token_set_ratio(user_input_formula, x, processor=utils.default_process))
# Take Top Similar Product. Take "Product Catalog" first
catalog = catalog.sort_values(by=['Similarity Score', 'Formula Similarity', 'Source'], ascending=[False, False, True]).head(1)
# Condition
if catalog['Similarity Score'].values[0] >= 80 and catalog['Source'].values[0] == 'Product Catalog' and (catalog['Formula Similarity'].values[0] == 100 or is_nan(catalog['Formula'].values[0])):
return f"[1] Product is Available in Catalog (SKU Registered as *{catalog['Registered Product'].values[0]}*)"
elif catalog['Similarity Score'].values[0] >= 80 and catalog['Source'].values[0] == 'Registered Fertilizers' and (catalog['Formula Similarity'].values[0] == 100 or is_nan(catalog['Formula'].values[0])):
return f"[2] Add as New Product (Registered in Kementan as *{catalog['Registered Product'].values[0]}*)"
elif catalog['Similarity Score'].values[0] >= 80 and catalog['Formula Similarity'].values[0] < 100:
return f"[3] Add as New Product (Similar to *{catalog['Registered Product'].values[0]}* in {catalog['Source'].values[0]} but with different formula)"
elif catalog['Similarity Score'].values[0] < 80:
if pipe_detect(user_input)[0]['label'] == 'Fertilizer' and pipe_detect(user_input)[0]['score'] > 0.8:
return f"[4] Add as New Product ({pipe_detect(user_input)[0]['score'] * 100}% probability of being a fertilizer)"
else:
return f"[5] Product might not be a Fertilizer ({np.round(pipe_detect(user_input)[0]['score'] * 100,2)}% probability of being a {pipe_detect(user_input)[0]['label']})"
else:
return "[6] Product is not a Fertilizer"
elif type == 'Training Mode':
# Same like above, but only match with catalog[catalog['Source'] == 'Product Catalog']['Registered Product']
catalog = catalog[catalog['Source'] == 'Product Catalog']
catalog['Similarity Score'] = catalog[product_name_catalog].apply(lambda x: fuzz.token_set_ratio(user_input, x, processor=utils.default_process))
catalog['Formula Similarity'] = catalog['Formula'].apply(lambda x: fuzz.token_set_ratio(user_input_formula, x, processor=utils.default_process))
# Take Top Similar Product
catalog = catalog.sort_values(by=['Similarity Score', 'Formula Similarity'], ascending=False).head(1)
return catalog['Registered Product'].values[0]
elif type == 'Probabilistic Search':
catalog = catalog[catalog['Source'] == 'Product Catalog']
# Based on probability
catalog['Concat Input'] = user_input + '[SEP]' + catalog['Registered Product'].astype(str)
catalog['Similarity Score'] = catalog['Concat Input'].apply(lambda x: pipe_match(x)[0]['score'])
catalog = catalog.sort_values(by=['Similarity Score'], ascending=False).head(1)
return f"{np.round(catalog['Similarity Score'].values[0] * 100,2)}% probability of being a {catalog['Registered Product'].values[0]}"
def app(input, type):
if input is None or type is None:
return "Please fill in the input and select the search type"
catalog = prepare_catalog()
return decision(input, type, catalog, "Registered Product")
# Initialize the app
demo = gr.Interface(
fn=app,
inputs=[
gr.Textbox(),
gr.Radio(["Fuzzy Search", "Probabilistic Search", "Training Mode"], type="value")
],
outputs="text",
examples= [
['Petro Nitrat 16-16-16','Fuzzy Search'],
['Petro Nitrat 15-15-15','Fuzzy Search'],
['Gramoxone 1 Liter','Fuzzy Search'],
['Indomie Goreng Aceh','Fuzzy Search']
],
title = 'Fertilizer Catalog Engine 🌽',
description = 'Catalog Search Engine and Decision Support System for Fertilizer Company',
article= """
### About The App
This app is built as a part of the Data Science Weekend (DSW) 2023 Challenge submission. This app aims to help fertilizer companies to map
free-text POS data of multiple types of products into their own fertilizer catalog. By using this app, the company will be able to
decide whether a product is already available in their catalog, or whether it is a new product that needs (and eligible) to be added to
the catalog.
### How Does it Work?
This app uses a combination of fuzzy matching and machine learning to determine whether a product is already available in the catalog or not.
When a product is not available in the catalog, we will use an IndoBERT model to determine if the product is a fertilizer and eligible to be
added to the catalog. Beforehand, we have fine-tuned the IndoBERT model using a combination of internal and external (web scraping) data, so the
model will be able to learn how fertilizer products (especially the local ones) look like.
### What are the Flags For?
The flag is a part of the "Active Transfer Learning" feature of this app when the user selects "Training Mode". When a user flags an output as "Correct" or "Incorrect",
the developer will be able to fine-tune the model using the user's input, hence improving the model's performance when the user selects "Probabilistic Search". So, please
help us to improve the model by flagging the prediction result 🙏
### I want to test multiple inputs at once!
You can also use our app via API by clicking the "Use via API" below. The API will give developer more flexibility to test multiple inputs
programmatically.
""",
api_name='search',
allow_flagging='manual',
flagging_options=["Correct","Incorrect"],
flagging_dir='flagging/',
theme = gr.themes.Soft()
)
# Run the app
if __name__ == "__main__":
demo.launch(show_api=True)