rairo commited on
Commit
77bd3e6
·
verified ·
1 Parent(s): 645a054

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +53 -35
app.py CHANGED
@@ -31,17 +31,23 @@ def drop_unnamed_columns(df):
31
  return df.loc[:, ~df.columns.str.contains("Unnamed", case=False, na=False)]
32
 
33
  def flatten_columns(df):
34
- """If the DataFrame has a MultiIndex for columns, flatten it."""
 
 
 
35
  if isinstance(df.columns, pd.MultiIndex):
36
- df.columns = [' '.join([str(x) for x in col if x is not None and x != 'nan']).strip()
37
- for col in df.columns.values]
 
 
 
38
  return df
39
 
40
  def read_excel_file(file, header_option=0):
41
  """
42
  Read an Excel file using the given header option,
43
- flatten and normalize its columns,
44
- drop columns whose header contains 'Unnamed',
45
  and drop completely empty rows/columns.
46
  """
47
  df = pd.read_excel(file, header=header_option)
@@ -54,7 +60,8 @@ def read_excel_file(file, header_option=0):
54
 
55
  def get_column(df, possible_names):
56
  """
57
- Return the first column name found in df.columns matching any name in possible_names (case-insensitive).
 
58
  """
59
  lower_cols = {col.lower(): col for col in df.columns}
60
  for name in possible_names:
@@ -62,19 +69,24 @@ def get_column(df, possible_names):
62
  return lower_cols[name.lower()]
63
  return None
64
 
65
- # --- Processing Functions for Each File ---
66
  def process_employee_data(df):
67
- """Process employee data: ensure a proper TIN and build Employee Name if needed."""
68
  df = normalize_columns(df)
69
- # If "Employee Name" is missing or all blank, try to build it.
 
70
  if 'Employee Name' not in df.columns or df['Employee Name'].isna().all():
71
  first_name = get_column(df, ["First Name", "First", "Forename"])
72
  last_name = get_column(df, ["Last Name", "Surname", "Family Name", "Last"])
73
  if first_name and last_name:
74
  df["Employee Name"] = df[first_name].apply(clean_name) + " " + df[last_name].apply(clean_name)
75
- # Standardize TIN from either 'TIN' or 'Personal ID of Employee'
76
- if 'TIN' in df.columns:
77
- df['TIN'] = df['TIN'].apply(standardize_tin)
 
 
 
 
78
  else:
79
  alt = get_column(df, ["Personal ID of Employee"])
80
  if alt:
@@ -85,10 +97,14 @@ def process_employee_data(df):
85
  return df
86
 
87
  def process_salary_data(df):
88
- """Process the salary (earnings) data (the master): standardize TIN and convert earnings figures to numeric."""
89
  df = normalize_columns(df)
90
- if 'TIN' in df.columns:
91
- df['TIN'] = df['TIN'].apply(standardize_tin)
 
 
 
 
92
  else:
93
  alt = get_column(df, ["TIN or Personal ID of Employee"])
94
  if alt:
@@ -96,6 +112,8 @@ def process_salary_data(df):
96
  df.drop(columns=[alt], inplace=True)
97
  else:
98
  raise KeyError("Salary data must contain a TIN column.")
 
 
99
  ignore = {'TIN', 'Employee Name', 'Currency'}
100
  for col in df.columns:
101
  if col not in ignore:
@@ -105,8 +123,11 @@ def process_salary_data(df):
105
  def process_paye_data(df):
106
  """Process PAYE data: standardize TIN and convert other columns to numeric."""
107
  df = normalize_columns(df)
108
- if 'TIN' in df.columns:
109
- df['TIN'] = df['TIN'].apply(standardize_tin)
 
 
 
110
  else:
111
  alt = get_column(df, ["TIN or Personal ID of Employee"])
112
  if alt:
@@ -114,6 +135,7 @@ def process_paye_data(df):
114
  df.drop(columns=[alt], inplace=True)
115
  else:
116
  raise KeyError("PAYE data must contain a TIN column.")
 
117
  ignore = {'TIN', 'Employed From date', 'Employed To date'}
118
  for col in df.columns:
119
  if col not in ignore:
@@ -123,17 +145,16 @@ def process_paye_data(df):
123
  # --- Merging Function ---
124
  def merge_dataframes(employee_df, salary_df, paye_df):
125
  """
126
- Use the salary file as the master.
127
  Left-join key employee fields onto salary data,
128
  then left-join PAYE data.
129
- Finally, drop any remaining columns whose header contains 'Unnamed'.
130
  """
131
- # Restrict employee data to key columns (if available).
132
- key_emp = ['TIN', 'Employee Name', 'First Name', 'Middle Name', 'Last Name',
133
- 'Birth Date', 'Employed From date', 'Employed To date', 'Position']
134
  emp_subset = employee_df[[col for col in key_emp if col in employee_df.columns]].copy()
135
 
136
- # Merge employee data (using a left join onto the salary file).
137
  merged_df = salary_df.merge(emp_subset, on='TIN', how='left', suffixes=('', '_emp'))
138
  for col in ['Employee Name', 'First Name', 'Middle Name', 'Last Name',
139
  'Birth Date', 'Employed From date', 'Employed To date', 'Position']:
@@ -142,9 +163,7 @@ def merge_dataframes(employee_df, salary_df, paye_df):
142
  merged_df[col] = merged_df[col].combine_first(merged_df[alt])
143
  merged_df.drop(columns=[alt], inplace=True)
144
 
145
- # Merge PAYE data.
146
  merged_df = merged_df.merge(paye_df, on='TIN', how='left', suffixes=('', '_paye'))
147
- # Combine any duplicate columns from PAYE.
148
  for col in list(merged_df.columns):
149
  if col.endswith('_paye'):
150
  base = col[:-5]
@@ -154,13 +173,9 @@ def merge_dataframes(employee_df, salary_df, paye_df):
154
  merged_df.rename(columns={col: base}, inplace=True)
155
  merged_df.drop(columns=[col], inplace=True)
156
 
157
- # Drop any columns with 'Unnamed' in the header.
158
  merged_df = drop_unnamed_columns(merged_df)
159
-
160
- # For numeric columns, fill remaining NaNs with 0.
161
  num_cols = merged_df.select_dtypes(include=[np.number]).columns
162
  merged_df[num_cols] = merged_df[num_cols].fillna(0)
163
-
164
  return merged_df
165
 
166
  # --- Main (Streamlit UI) ---
@@ -170,24 +185,27 @@ def main():
170
  **Upload the following files:**
171
 
172
  1. **Employee Information File** (single header row)
173
- 2. **Salary (Earnings) Information File** – this file is the master; if it has two header rows (e.g. one for metric and one for currency), it will be read accordingly.
174
  3. **PAYE Information File** (single header row)
 
 
 
175
  """)
176
 
177
  employee_file = st.file_uploader("Upload Employee File", type=['xlsx', 'xls'])
178
- # For the salary file, we use header_option=[0,1] to capture two header rows.
179
  salary_file = st.file_uploader("Upload Salary (Earnings) File (Master)", type=['xlsx', 'xls'])
180
  paye_file = st.file_uploader("Upload PAYE File", type=['xlsx', 'xls'])
181
 
182
  if employee_file and salary_file and paye_file:
183
  try:
184
  employee_df = read_excel_file(employee_file, header_option=0)
185
- salary_df = read_excel_file(salary_file, header_option=[0,1])
186
- paye_df = read_excel_file(paye_file, header_option=0)
 
187
 
188
  employee_df = process_employee_data(employee_df)
189
- salary_df = process_salary_data(salary_df)
190
- paye_df = process_paye_data(paye_df)
191
 
192
  final_df = merge_dataframes(employee_df, salary_df, paye_df)
193
 
 
31
  return df.loc[:, ~df.columns.str.contains("Unnamed", case=False, na=False)]
32
 
33
  def flatten_columns(df):
34
+ """
35
+ If the DataFrame has a MultiIndex for columns, flatten it.
36
+ This version joins non-empty levels with a space.
37
+ """
38
  if isinstance(df.columns, pd.MultiIndex):
39
+ new_cols = []
40
+ for col in df.columns:
41
+ parts = [str(x).strip() for x in col if pd.notna(x) and str(x).strip() not in ["", "nan"]]
42
+ new_cols.append(" ".join(parts))
43
+ df.columns = new_cols
44
  return df
45
 
46
  def read_excel_file(file, header_option=0):
47
  """
48
  Read an Excel file using the given header option,
49
+ flatten multi-index columns, normalize column names,
50
+ drop columns with 'Unnamed' in their header,
51
  and drop completely empty rows/columns.
52
  """
53
  df = pd.read_excel(file, header=header_option)
 
60
 
61
  def get_column(df, possible_names):
62
  """
63
+ Return the first matching column name from df.columns (case-insensitive)
64
+ that appears in possible_names.
65
  """
66
  lower_cols = {col.lower(): col for col in df.columns}
67
  for name in possible_names:
 
69
  return lower_cols[name.lower()]
70
  return None
71
 
72
+ # --- Processing Functions ---
73
  def process_employee_data(df):
74
+ """Process employee data: build Employee Name (if needed) and standardize TIN."""
75
  df = normalize_columns(df)
76
+
77
+ # Build Employee Name if not present or all blank.
78
  if 'Employee Name' not in df.columns or df['Employee Name'].isna().all():
79
  first_name = get_column(df, ["First Name", "First", "Forename"])
80
  last_name = get_column(df, ["Last Name", "Surname", "Family Name", "Last"])
81
  if first_name and last_name:
82
  df["Employee Name"] = df[first_name].apply(clean_name) + " " + df[last_name].apply(clean_name)
83
+
84
+ # Standardize TIN: look for "TIN" first, else "Personal ID of Employee"
85
+ tin_col = get_column(df, ["TIN"])
86
+ if tin_col:
87
+ df['TIN'] = df[tin_col].apply(standardize_tin)
88
+ if tin_col != "TIN":
89
+ df.rename(columns={tin_col: "TIN"}, inplace=True)
90
  else:
91
  alt = get_column(df, ["Personal ID of Employee"])
92
  if alt:
 
97
  return df
98
 
99
  def process_salary_data(df):
100
+ """Process the salary (earnings) data (master): standardize TIN and convert earnings figures to numeric."""
101
  df = normalize_columns(df)
102
+ # Use get_column to find TIN (case-insensitive)
103
+ tin_col = get_column(df, ["TIN"])
104
+ if tin_col:
105
+ df['TIN'] = df[tin_col].apply(standardize_tin)
106
+ if tin_col != "TIN":
107
+ df.rename(columns={tin_col: "TIN"}, inplace=True)
108
  else:
109
  alt = get_column(df, ["TIN or Personal ID of Employee"])
110
  if alt:
 
112
  df.drop(columns=[alt], inplace=True)
113
  else:
114
  raise KeyError("Salary data must contain a TIN column.")
115
+
116
+ # Convert non-key columns to numeric.
117
  ignore = {'TIN', 'Employee Name', 'Currency'}
118
  for col in df.columns:
119
  if col not in ignore:
 
123
  def process_paye_data(df):
124
  """Process PAYE data: standardize TIN and convert other columns to numeric."""
125
  df = normalize_columns(df)
126
+ tin_col = get_column(df, ["TIN"])
127
+ if tin_col:
128
+ df['TIN'] = df[tin_col].apply(standardize_tin)
129
+ if tin_col != "TIN":
130
+ df.rename(columns={tin_col: "TIN"}, inplace=True)
131
  else:
132
  alt = get_column(df, ["TIN or Personal ID of Employee"])
133
  if alt:
 
135
  df.drop(columns=[alt], inplace=True)
136
  else:
137
  raise KeyError("PAYE data must contain a TIN column.")
138
+
139
  ignore = {'TIN', 'Employed From date', 'Employed To date'}
140
  for col in df.columns:
141
  if col not in ignore:
 
145
  # --- Merging Function ---
146
  def merge_dataframes(employee_df, salary_df, paye_df):
147
  """
148
+ Use the salary file as master.
149
  Left-join key employee fields onto salary data,
150
  then left-join PAYE data.
151
+ Drop any remaining "Unnamed" columns.
152
  """
153
+ key_emp = ['TIN', 'Employee Name', 'First Name', 'Middle Name',
154
+ 'Last Name', 'Birth Date', 'Employed From date',
155
+ 'Employed To date', 'Position']
156
  emp_subset = employee_df[[col for col in key_emp if col in employee_df.columns]].copy()
157
 
 
158
  merged_df = salary_df.merge(emp_subset, on='TIN', how='left', suffixes=('', '_emp'))
159
  for col in ['Employee Name', 'First Name', 'Middle Name', 'Last Name',
160
  'Birth Date', 'Employed From date', 'Employed To date', 'Position']:
 
163
  merged_df[col] = merged_df[col].combine_first(merged_df[alt])
164
  merged_df.drop(columns=[alt], inplace=True)
165
 
 
166
  merged_df = merged_df.merge(paye_df, on='TIN', how='left', suffixes=('', '_paye'))
 
167
  for col in list(merged_df.columns):
168
  if col.endswith('_paye'):
169
  base = col[:-5]
 
173
  merged_df.rename(columns={col: base}, inplace=True)
174
  merged_df.drop(columns=[col], inplace=True)
175
 
 
176
  merged_df = drop_unnamed_columns(merged_df)
 
 
177
  num_cols = merged_df.select_dtypes(include=[np.number]).columns
178
  merged_df[num_cols] = merged_df[num_cols].fillna(0)
 
179
  return merged_df
180
 
181
  # --- Main (Streamlit UI) ---
 
185
  **Upload the following files:**
186
 
187
  1. **Employee Information File** (single header row)
188
+ 2. **Salary (Earnings) Information File** – this file is the master.
189
  3. **PAYE Information File** (single header row)
190
+
191
+ *If your salary file has two header rows (e.g. one for metrics and one for currency),
192
+ it will be read with header_option=[0,1].*
193
  """)
194
 
195
  employee_file = st.file_uploader("Upload Employee File", type=['xlsx', 'xls'])
 
196
  salary_file = st.file_uploader("Upload Salary (Earnings) File (Master)", type=['xlsx', 'xls'])
197
  paye_file = st.file_uploader("Upload PAYE File", type=['xlsx', 'xls'])
198
 
199
  if employee_file and salary_file and paye_file:
200
  try:
201
  employee_df = read_excel_file(employee_file, header_option=0)
202
+ # Read salary file with header_option=[0,1] to capture two header rows, if present.
203
+ salary_df = read_excel_file(salary_file, header_option=[0,1])
204
+ paye_df = read_excel_file(paye_file, header_option=0)
205
 
206
  employee_df = process_employee_data(employee_df)
207
+ salary_df = process_salary_data(salary_df)
208
+ paye_df = process_paye_data(paye_df)
209
 
210
  final_df = merge_dataframes(employee_df, salary_df, paye_df)
211