backend / app /categorization /categorizer_list.py
palexis3's picture
Add more experimentation code
3151e90
Raw
History Blame
5.57 kB
import os
from datetime import datetime
from fastapi import HTTPException
import pandas as pd
import codecs
from app.categorization.config import CATEGORY_REFERENCE_OUTPUT_FILE
from app.categorization.categorizer import llm_list_categorizer, fuzzy_match_list_categorizer
async def categorize_list(df: pd.DataFrame) -> pd.DataFrame:
"""Asynchronously categorize a list of transactions.
This function categorizes a list of transactions using a combination of fuzzy matching
and a language model. It looks up new transaction descriptions in the reference file
(a combination of user input and previous executions) to minimize API calls.
Any uncategorized transactions are sent to the language model, and new description-category
pairs are added to the reference file.
Args:
df (pd.DataFrame): The list of transactions to categorize.
Returns:
pd.DataFrame: The original DataFrame with an additional column for the category.
"""
result = {"df": pd.DataFrame(), "description_category": "", "description_category_pairs": "", "uncategorized_descriptions": "", "categorized_descriptions": "", "error": ""}
try:
if not os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
os.makedirs(CATEGORY_REFERENCE_OUTPUT_FILE)
if os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
# Read description-category pairs from the reference file
# TODO FIRST ISSUE: Must save categorize_list categorized_descriptions items to output file
description_category_pairs = pd.read_csv(
CATEGORY_REFERENCE_OUTPUT_FILE, names=['name_description', 'category'], header=0
)
print(f"\ncategorize_list description_category_pairs \n{description_category_pairs}\n")
df['name_description'] = df['name_description'].astype(str)
df['category'] = df['category'].astype(str)
df['type'] = df['type'].astype(str)
# df['amount'] = df['amount'].str.strip().replace("\s", "").astype(float)
# df['transaction_date'] = pd.to_datetime(df['transaction_date'].str.strip().replace("\s", ""), format='%d/%m/%Y')
# Extract only descriptions for faster matching
# description_category_pairs.columns = description_category_pairs.columns.str.strip()
# descriptions = description_category_pairs['name_description'].values
descriptions = df['name_description'].values
print(f"\ncategorize_list descriptions \n{descriptions}\n")
result['description_category'] = descriptions
result['df'] = df
temp_df = df
print(f"\ncategorize_list df \n{df}\n")
# Use fuzzy matching to find similar descriptions and assign the category
# TODO SECOND ISSUE: Why isn't category being updated
df['category'] = df['name_description'].apply(
fuzzy_match_list_categorizer,
args=(descriptions, description_category_pairs)
)
print(f"\ncategorize_list setting fuzzy match list: \n{df}\n")
# Filter out uncategorized transactions, deduplicate, and sort by description
uncategorized_descriptions = (
df[df['category'].isnull()]
.drop_duplicates(subset=['name_description'])
.sort_values(by=['name_description'])
)
result['uncategorized_descriptions'] = uncategorized_descriptions
print(f"\ncategorize_list uncategorized_descriptions: \n{uncategorized_descriptions}\n")
# Ask the language model to categorize the remaining descriptions
if not uncategorized_descriptions.empty:
categorized_descriptions = await llm_list_categorizer(
uncategorized_descriptions[['name_description', 'category']]
)
categorized_descriptions.dropna(inplace=True)
result['categorized_descriptions'] = categorized_descriptions
print(f"\ncategorize_list categorized_descriptions: \n{categorized_descriptions}\n")
# Update the category for uncategorized transactions based on the language model results
if not categorized_descriptions.empty:
# temp_df['category'] = temp_df['name_description'].map(categorized_descriptions.set_index('name_description')['category'])
print(f"\ncategorize_list current dataframe:\n {temp_df}\n")
# df['category'] = df['category'].fillna(
# df['name_description'].map(
# categorized_descriptions.set_index('name_description')['category']
# )
# )
# print(f"\ncategorize_list categorized_description df[category]: \n{df['category']}\n")
# Fill remaining NaN values in 'category' with 'Other'
with pd.option_context("future.no_silent_downcasting", True):
df['category'] = df['category'].fillna('Other').infer_objects(copy=False)
# print(f"\ncategorize_list Fill remaining NaN df[category]: \n{df['category']}\n")
return df
except Exception as e:
# Return an error indicator and exception info
print(f"ERROR categorizer_list: {str(e)}")
result["error"] = str(e)
raise HTTPException(status_code = 500, detail=f"categorize_list result: {result}")