Spaces:
Sleeping
Sleeping
| 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}") |