File size: 11,399 Bytes
29c7ba2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# data_processor.py

import pandas as pd
import numpy as np
from typing import Union, Dict, List, Tuple
from datetime import datetime, timedelta

class DataProcessor:
    """Handles ERP sales data processing for concentration analysis"""
    
    def __init__(self):
        """Initialize the data processor"""
        self.supported_formats = ['csv', 'xlsx', 'xls']
        self.date_columns = ['date', 'transaction_date', 'invoice_date', 'order_date']
        
    def load_data(self, file) -> pd.DataFrame:
        """Load data from uploaded file"""
        if file.name.endswith('.csv'):
            df = pd.read_csv(file)
        elif file.name.endswith(('.xlsx', '.xls')):
            df = pd.read_excel(file)
        else:
            raise ValueError(f"Unsupported file format. Please upload {', '.join(self.supported_formats)}")
        
        return df
    
    def process_erp_data(self, df: pd.DataFrame, 
                       date_range: Tuple[str, str] = None,
                       customer_col: str = None,
                       amount_col: str = None) -> pd.DataFrame:
        """Process ERP sales data for concentration analysis"""
        # If columns not specified, try to auto-detect
        if customer_col is None:
            customer_col = self._find_customer_column(df)
        
        if amount_col is None:
            amount_col = self._find_amount_column(df)
        
        # Standardize date column
        date_col = self._find_date_column(df)
        if date_col:
            df[date_col] = pd.to_datetime(df[date_col], errors='coerce')
            
            # Filter by date range if specified
            if date_range:
                start_date, end_date = date_range
                start_date = pd.to_datetime(start_date)
                end_date = pd.to_datetime(end_date)
                df = df[(df[date_col] >= start_date) & (df[date_col] <= end_date)]
        
        # Handle transaction types if present
        transaction_type_col = self._find_transaction_type_column(df)
        if transaction_type_col:
            # Identify credits/returns and adjust amounts
            credit_mask = df[transaction_type_col].str.lower().isin(['credit', 'return', 'refund', 'credit memo'])
            if credit_mask.any():
                df.loc[credit_mask, amount_col] = -abs(df.loc[credit_mask, amount_col])
        
        # Group by customer to get total revenue
        customer_summary = df.groupby(customer_col)[amount_col].sum().reset_index()
        
        # Rename columns to standardized format
        customer_summary.columns = ['customer', 'revenue']
        
        # Clean the data
        customer_summary['revenue'] = self._clean_revenue_data(customer_summary['revenue'])
        
        # Remove customers with zero or negative total revenue
        customer_summary = customer_summary[customer_summary['revenue'] > 0].copy()
        
        # Sort by revenue (descending)
        customer_summary = customer_summary.sort_values('revenue', ascending=False).reset_index(drop=True)
        
        return customer_summary
    
    def generate_sample_erp_data(self, n_transactions: int = 5000) -> pd.DataFrame:
        """Generate realistic sample ERP sales data"""
        np.random.seed(42)
        
        # Generate customer base with different sizes
        customers = []
        
        # Large enterprise customers
        for i in range(5):
            customers.append({
                'customer_id': f'ENT{i+1:03d}',
                'customer_name': f'Enterprise Corp {i+1}',
                'size_factor': np.random.uniform(8, 15)  # Large transaction factor
            })
        
        # Medium businesses
        for i in range(20):
            customers.append({
                'customer_id': f'MID{i+1:03d}',
                'customer_name': f'Midsize Inc {i+1}',
                'size_factor': np.random.uniform(3, 8)   # Medium transaction factor
            })
        
        # Small businesses
        for i in range(75):
            customers.append({
                'customer_id': f'SML{i+1:03d}',
                'customer_name': f'Small Company {i+1}',
                'size_factor': np.random.uniform(0.5, 3) # Small transaction factor
            })
        
        # Create transactions
        transactions = []
        start_date = datetime(2023, 1, 1)
        end_date = datetime(2023, 12, 31)
        date_range = (end_date - start_date).days
        
        products = [
            {'id': 'PRD001', 'name': 'Server License', 'base_price': 8000},
            {'id': 'PRD002', 'name': 'Desktop License', 'base_price': 350},
            {'id': 'PRD003', 'name': 'Cloud Service Basic', 'base_price': 1200},
            {'id': 'PRD004', 'name': 'Cloud Service Premium', 'base_price': 4500},
            {'id': 'PRD005', 'name': 'Support Contract', 'base_price': 2000},
            {'id': 'PRD006', 'name': 'Training Package', 'base_price': 3000},
            {'id': 'PRD007', 'name': 'Hardware Module A', 'base_price': 1500},
            {'id': 'PRD008', 'name': 'Hardware Module B', 'base_price': 2500},
        ]
        
        sales_reps = [
            'John Smith', 'Sarah Johnson', 'Michael Brown', 'Jennifer Davis',
            'Robert Wilson', 'Lisa Moore', 'David Taylor', 'Jessica Anderson'
        ]
        
        regions = ['North America', 'Europe', 'Asia Pacific', 'Latin America']
        
        channels = ['Direct', 'Partner', 'Distributor', 'Online']
        
        for i in range(n_transactions):
            # Select random customer with weighted probability based on size
            customer_idx = np.random.choice(
                range(len(customers)), 
                p=[c['size_factor']/sum(c['size_factor'] for c in customers) for c in customers]
            )
            customer = customers[customer_idx]
            
            # Random transaction date
            transaction_date = start_date + timedelta(days=np.random.randint(0, date_range))
            
            # Random product
            product = products[np.random.randint(0, len(products))]
            
            # Random quantity (larger customers tend to order more)
            quantity = max(1, int(np.random.normal(customer['size_factor'], customer['size_factor']/2)))
            
            # Calculate amount (with some random variation)
            unit_price = product['base_price'] * np.random.uniform(0.9, 1.1)  # Some price variation
            amount = unit_price * quantity
            
            # Occasionally create returns/credits (5% of transactions)
            transaction_type = 'Sale'
            if np.random.random() < 0.05:
                transaction_type = 'Return'
                amount = -amount * np.random.uniform(0.1, 1.0)  # Partial return in most cases
            
            # Other random attributes
            sales_rep = np.random.choice(sales_reps)
            region = np.random.choice(regions)
            channel = np.random.choice(channels)
            
            # Create transaction record
            transactions.append({
                'transaction_id': f'TRX{i+1:06d}',
                'transaction_date': transaction_date,
                'customer_id': customer['customer_id'],
                'customer_name': customer['customer_name'],
                'product_id': product['id'],
                'product_name': product['name'],
                'quantity': quantity,
                'unit_price': unit_price,
                'amount': amount,
                'transaction_type': transaction_type,
                'sales_rep': sales_rep,
                'region': region,
                'channel': channel,
                'invoice_number': f'INV-{transaction_date.strftime("%Y%m")}-{i+1:04d}'
            })
        
        return pd.DataFrame(transactions)
    
    def _find_customer_column(self, df: pd.DataFrame) -> str:
        """Find the customer name column"""
        possible_names = ['customer', 'customer name', 'customer_name', 'client', 
                        'client name', 'client_name', 'company', 'company name']
        
        for col in df.columns:
            if col.lower() in possible_names:
                return col
        
        # Try to find columns containing 'customer' or 'client'
        for col in df.columns:
            if 'customer' in col.lower() or 'client' in col.lower():
                return col
        
        raise ValueError("Could not identify customer column")
    
    def _find_amount_column(self, df: pd.DataFrame) -> str:
        """Find the transaction amount column"""
        possible_names = ['amount', 'total', 'revenue', 'sales', 'price', 
                        'total_amount', 'extended_price', 'net_amount', 'line_total']
        
        for col in df.columns:
            if col.lower() in possible_names:
                return col
        
        # Try to find columns containing 'amount', 'total' or 'price'
        for col in df.columns:
            if 'amount' in col.lower() or 'total' in col.lower() or 'price' in col.lower():
                # Check if column is numeric
                if np.issubdtype(df[col].dtype, np.number):
                    return col
        
        # If still not found, look for any numeric columns
        numeric_cols = df.select_dtypes(include=[np.number]).columns
        if len(numeric_cols) > 0:
            return numeric_cols[0]
        
        raise ValueError("Could not identify amount column")
    
    def _find_date_column(self, df: pd.DataFrame) -> str:
        """Find the transaction date column"""
        for col in df.columns:
            if col.lower() in self.date_columns:
                return col
        
        # Try to find columns containing 'date'
        for col in df.columns:
            if 'date' in col.lower():
                return col
        
        # If not found, check if any column has datetime-like values
        for col in df.columns:
            try:
                pd.to_datetime(df[col].iloc[0])
                return col
            except:
                continue
        
        return None  # It's possible there is no date column
    
    def _find_transaction_type_column(self, df: pd.DataFrame) -> str:
        """Find the transaction type column (for identifying returns/credits)"""
        possible_names = ['transaction_type', 'type', 'document_type', 'invoice_type']
        
        for col in df.columns:
            if col.lower() in possible_names:
                return col
        
        # Try to find columns containing 'type'
        for col in df.columns:
            if 'type' in col.lower():
                # Check if it has typical transaction type values
                if df[col].astype(str).str.lower().isin(['sale', 'credit', 'return', 'invoice']).any():
                    return col
        
        return None  # It's possible there is no transaction type column
    
    def _clean_revenue_data(self, revenue_series: pd.Series) -> pd.Series:
        """Clean and standardize revenue data"""
        # If string, remove currency symbols and convert
        if revenue_series.dtype == 'object':
            revenue_series = revenue_series.astype(str).str.replace(',', '')
            revenue_series = pd.to_numeric(revenue_series, errors='coerce')
        
        return revenue_series.fillna(0)