File size: 5,566 Bytes
e612627
 
 
e506f4d
 
e612627
c37c62e
e612627
 
 
 
 
c37c62e
e612627
 
 
 
 
 
 
 
 
c37c62e
e612627
 
 
 
c37c62e
e612627
e506f4d
 
 
 
 
 
c37c62e
 
e506f4d
c37c62e
e506f4d
c37c62e
 
 
 
 
 
 
 
e506f4d
c37c62e
52b3c48
c37c62e
 
52b3c48
 
c37c62e
 
 
 
e506f4d
 
c37c62e
 
e506f4d
52b3c48
e612627
 
c37c62e
 
e506f4d
 
c37c62e
e506f4d
 
 
 
c37c62e
e506f4d
 
 
 
 
 
 
 
 
c37c62e
e506f4d
 
 
c37c62e
3151e90
c37c62e
 
 
 
 
 
 
e506f4d
 
c37c62e
 
 
e506f4d
c37c62e
e506f4d
 
 
c37c62e
e506f4d
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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}")