Projet

Général

Profil

US #1207 » compensation_service (1).py

Tarek AOUADI, 16/12/2025 17:27

 
1
from models.compensation import CompensationInput
2
import numpy as np
3
import pandas as pd
4
from sqlalchemy import create_engine
5
from config import Config
6
from collections import OrderedDict
7

    
8
class CompensationsService:
9

    
10
    @staticmethod
11
    def get_specifique_compensations(filters, company_id):
12
        """
13
        Fetch specific compensations based on filters and company ID.
14
        
15
        If `postes`, `sectors`, or `company_ids` are empty, they are removed from the query condition.
16
        
17
        Args:
18
            filters (dict): Filters containing "jobs", "business_sector", and "companies".
19
            company_id (int): ID of the company to include in the query.
20
        
21
        Returns:
22
            pd.DataFrame: DataFrame containing the fetched compensation data.
23
        """
24
        try:
25
            # Extract filters with defaults
26
            postes = filters.get("jobs", [])
27
            sectors = filters.get("business_sector", [])
28
            company_ids = filters.get("companies", []) 
29
            if len(company_ids)!=0:
30
                company_ids=company_ids+ [company_id]
31
            else:
32
                pass
33

    
34
            # Initialize query components
35
            query_conditions = []
36

    
37
            # Helper function to add conditions dynamically
38
            def add_in_condition(column_name, values):
39
                if values:  # Only add the condition if values are not empty
40
                    placeholders = ", ".join(f'"{value}"' for value in values)
41
                    query_conditions.append(f"{column_name} IN ({placeholders})")
42

    
43
            add_in_condition("s.businessSectorCode", sectors)
44
            add_in_condition("c.companyId", company_ids)
45
            add_in_condition("c.job", postes)
46
            where_clause = " AND ".join(query_conditions) if query_conditions else "1=1"
47

    
48
            query = f"""
49
                SELECT c.companyId, c.matricule, c.gross_annual_salary, c.meal_allowance, 
50
                    c.location_allowance, c.transportation_allowance, c.other_bonuses_awarded_last_year, 
51
                    c.fixed_allowances, c.total_amount_performance_bonus_awarded, 
52
                    c.total_amount_sales_bonus_awarded, c.total_amount_sales_commissions_awarded, 
53
                    c.schooling_allowance, c.seniority_bonus, c.function_code, c.subfunction_code, 
54
                    c.collaborative_path, c.grade, c.country, c.city, c.site, c.department, c.internal_grade, c.gender, c.job, c.function
55
                FROM company_compensation c
56
                LEFT JOIN company s ON c.companyId = s.id
57
                WHERE {where_clause};
58
            """
59

    
60
            # Execute the query
61
            #engine=Config.get_db_engine() 
62
            #with engine.connect() as connection:
63
            with Config.get_db_connection() as connection:
64
                result = pd.read_sql_query(query, connection)
65

    
66
            return result
67

    
68
        except Exception as e:
69
            raise RuntimeError(f"Error fetching specific compensations: {e}")
70

    
71
    #Calcul
72
    @staticmethod
73
    def calculate_salary_statistics(df, var):
74
        """
75
        Calculate salary statistics for the given variable (column) in the DataFrame.
76

    
77
        Parameters:
78
        - df: The DataFrame containing the data for the group.
79
        - var: The column name for salary data.
80

    
81
        Returns:
82
        - A dictionary of calculated salary statistics.
83
        """
84
        
85
        # Extract the salary column as a Series
86

    
87
        salaries = df[var]
88
        # Calculate statistics
89
        total_salary = salaries.sum()
90
        num_salaries = len(salaries)
91
        average_salary = total_salary / num_salaries
92

    
93
        # Median
94
        median_salary = salaries.median()
95
        # Min and Max
96
        min_salary = salaries.min()
97
        max_salary = salaries.max()
98
        
99
        # Standard Deviation and Percentiles
100

    
101
        std_dev_salary = salaries.std()
102

    
103
        percentile_25 = salaries.quantile(0.25)
104
        percentile_75 = salaries.quantile(0.75)
105

    
106
        return {
107
            #'total_salaries': total_salary,
108
            f'{var}_average': average_salary,
109
            'number_of_incumbents': num_salaries,
110
            f'{var}_25': percentile_25,
111
            f'{var}_50': median_salary,
112
            f'{var}_75': percentile_75,
113
            #f'{var}_std_dev': std_dev_salary,
114
            #f'{var}_min': min_salary,
115
            #f'{var}_max': max_salary,
116
        }
117

    
118
    @staticmethod
119
    def calculate_base_salary_diff(var,data):
120
      
121
        # Drop rows with NaN values in the required columns        
122
        data[var+'_diff_25_perc']=100*(data[var+'_your_data']-data[var+'_25'])/data[var+'_25']
123
        data[var+'_diff_50_perc']=100*(data[var+'_your_data']-data[var+'_50'])/data[var+'_50']
124
        data[var+'_diff_75_perc']=100*(data[var+'_your_data']-data[var+'_75'])/data[var+'_75']
125

    
126
        return data
127
        
128
    def calculate_display_var(vars,data,condition):
129
        output=pd.DataFrame()
130
        for var in vars:
131

    
132
            output[var]=data.groupby(condition)[var].agg(lambda x: ", ".join(x.unique()))
133

    
134
        return output.reset_index()
135
        
136
    @staticmethod
137
    def get_compensation_general_graph(general_graphcode, companyId, compensations,round):
138
        results = []
139
        company_data = compensations[compensations["companyId"]==companyId]
140
        # Calculate data only for requested graphs
141
        if 'gap_general' in general_graphcode:
142
            # Calculate Average Salary metrics
143
            Brut_fixe_societe_median = company_data["base_salary"].median()
144
            Brut_fixe_gi_median = compensations["base_salary"].median()
145
            Brut_fixe_variation = (Brut_fixe_societe_median/Brut_fixe_gi_median-1) if pd.notna(Brut_fixe_societe_median/Brut_fixe_gi_median-1) else 0 
146
            
147
            Brut_total_societe_median = company_data["total_compensation"].median()
148
            Brut_total_gi_median = compensations["total_compensation"].median()
149
            Brut_total_variation = (Brut_total_societe_median/Brut_total_gi_median -1) if pd.notna(Brut_total_societe_median/Brut_total_gi_median -1) else 0
150
            Brut_total_societe_median = Brut_total_societe_median if pd.notna(Brut_total_societe_median) else 0
151
            Brut_fixe_societe_median = Brut_fixe_societe_median if pd.notna(Brut_fixe_societe_median) else 0
152

    
153

    
154
            gap_general_data =  {
155
                    'name': 'gap_general', 
156
                    'data': [
157
                        {
158
                            "a_salary": "Brut Fixe", 
159
                            "b_society_median": f"{Brut_fixe_societe_median:,.{round}f}".replace(',', ' '),
160
                            "c_gap":"{:.2%}".format(Brut_fixe_variation),
161
                            "d_market_median": f"{Brut_fixe_gi_median:,.{round}f}".replace(',', ' '),
162
                          
163
                        },
164
                        {
165
                            "a_salary": "Brut Total", 
166
                            "b_society_median": f"{Brut_total_societe_median:,.{round}f}".replace(',', ' '),
167
                            "c_gap": "{:.2%}".format(Brut_total_variation),
168
                            "d_market_median": f"{Brut_total_gi_median:,.{round}f}".replace(',', ' ')
169
                            
170
                        }
171
                    ]
172
                }
173
            results.append(( gap_general_data))
174
        
175
        if 'general_internal_gap_fixed' in general_graphcode:
176
            # Calculate salary range metrics
177
            Brut_fixe_societe_median = company_data["base_salary"].median()
178

    
179
            Brut_total_societe_median = company_data["total_compensation"].median()
180
            fixed_base_salary_max = company_data["base_salary"].max()
181
            fixed_base_salary_max_perc = (fixed_base_salary_max/Brut_fixe_societe_median-1) if pd.notna(fixed_base_salary_max/Brut_fixe_societe_median-1) else 0  
182
            fixed_base_salary_min = company_data["base_salary"].min() if pd.notna(company_data["base_salary"].min()) else 0   
183
            fixed_base_salary_min_perc =  (fixed_base_salary_min/Brut_fixe_societe_median-1) if pd.notna( fixed_base_salary_min/Brut_fixe_societe_median-1) else 0  
184
            fixed_range = (1-fixed_base_salary_min/fixed_base_salary_max) if pd.notna(1-fixed_base_salary_min/fixed_base_salary_max) else 0   
185
            
186
            total_base_salary_max = company_data["total_compensation"].max()
187
            total_base_salary_max_perc = (total_base_salary_max/Brut_total_societe_median-1) if pd.notna(total_base_salary_max/Brut_total_societe_median-1) else 0  
188
            total_base_salary_min =company_data["total_compensation"].min() if pd.notna(company_data["total_compensation"].min()) else 0  
189
            total_base_salary_min_perc = (total_base_salary_min/Brut_total_societe_median-1) if pd.notna(total_base_salary_min/Brut_total_societe_median-1) else 0  
190
            total_range = (1-total_base_salary_min/total_base_salary_max) if pd.notna(1-total_base_salary_min/total_base_salary_max) else 0  
191
            
192
            Brut_total_societe_median = Brut_total_societe_median if pd.notna(Brut_total_societe_median) else 0
193
            Brut_fixe_societe_median = Brut_fixe_societe_median if pd.notna(Brut_fixe_societe_median) else 0
194
            fixed_base_salary_max = fixed_base_salary_max if pd.notna(fixed_base_salary_max) else 0
195
            total_base_salary_max = total_base_salary_max if pd.notna(total_base_salary_max) else 0
196
            
197
            internal_gap_data = {
198
                    'name': 'general_internal_gap_fixed', 
199
                    'data': [{
200
                      "fixed_median": f"{Brut_fixe_societe_median:,.{round}f}".replace(',', ' '),
201
                        "fixed_base_salary_max": f"{fixed_base_salary_max:,.{round}f}".replace(',', ' '),
202
                        "fixed_base_salary_max_perc": "{:.2%}".format(fixed_base_salary_max_perc),
203
                        "fixed_base_salary_min": f"{fixed_base_salary_min:,.{round}f}".replace(',', ' '),
204
                        "fixed_base_salary_min_perc": "{:.2%}".format(fixed_base_salary_min_perc),
205
                        "fixed_range": "{:.2%}".format(fixed_range)}, 
206
                        {"total_median": f"{Brut_total_societe_median:,.{round}f}".replace(',', ' '),
207
                        "total_base_salary_max": f"{total_base_salary_max:,.{round}f}".replace(',', ' '),
208
                        "total_base_salary_max_perc": "{:.2%}".format(total_base_salary_max_perc),
209
                        "total_base_salary_min":  f"{total_base_salary_min:,.{round}f}".replace(',', ' '),
210
                        "total_base_salary_min_perc": "{:.2%}".format(total_base_salary_min_perc),
211
                        "total_range": "{:.2%}".format(total_range)
212
                    }]
213
                }
214
            results.append((internal_gap_data))
215
        
216
        # Handle unknown graph codes
217
        for code in general_graphcode:
218
            if code not in ['gap_general', 'general_internal_gap_fixed']:
219
                results=results
220
        
221
        return results
222

    
223
    @staticmethod
224
    def get_compensation_graph_by_criteria(specifique_graphcode, condition,companyId, compensations,round):
225
        # Filter data for the specified company once (for efficiency)
226
        company_data = compensations[compensations["companyId"] == companyId]
227

    
228
        # Calculate gaps only if needed by requested graphs
229
        required_graphs = set(specifique_graphcode)
230
        results = {
231
            'repartition_gap': 0,
232
            'repartition_data_bf': 0,
233
            'repartition_data_bt': 0
234
        }
235
        
236
        # Calculate gaps vs other companies
237
        grouped_compensation=compensations.groupby(condition)["base_salary"].count().reset_index()
238
        grouped_compensation["company_based_salary"]=company_data.groupby(condition)["base_salary"].mean().reset_index()["base_salary"]
239
        grouped_compensation["others_based_salary"]= compensations.groupby(condition)["base_salary"].mean().reset_index()["base_salary"]
240
        grouped_compensation["fixe_brut_ecart"] = grouped_compensation["company_based_salary"]/ grouped_compensation["others_based_salary"]- 1
241
        
242
        grouped_compensation["company_total_compensation"]=company_data.groupby(condition)["total_compensation"].mean().reset_index()["total_compensation"]
243
        grouped_compensation["others_total_compensation"]= compensations.groupby(condition)["total_compensation"].mean().reset_index()["total_compensation"]
244
        grouped_compensation["total_brut_ecart"] =grouped_compensation["company_total_compensation"]/ grouped_compensation["others_total_compensation"]- 1
245

    
246
        # --- Compute only what's needed ---
247
        if 'repartition_gap' in required_graphs:
248

    
249
            # Compute mean gaps
250
            pos_fixe = grouped_compensation[grouped_compensation["fixe_brut_ecart"] >= 0]["fixe_brut_ecart"].mean() if pd.notna(grouped_compensation[grouped_compensation["fixe_brut_ecart"] >= 0]["fixe_brut_ecart"].mean()) else 0
251
            neg_fixe = grouped_compensation[grouped_compensation["fixe_brut_ecart"] <= 0]["fixe_brut_ecart"].mean() if pd.notna(grouped_compensation[grouped_compensation["fixe_brut_ecart"] <= 0]["fixe_brut_ecart"].mean()) else 0 
252
            pos_total = grouped_compensation[grouped_compensation["total_brut_ecart"] >= 0]["total_brut_ecart"].mean() if pd.notna(grouped_compensation[grouped_compensation["total_brut_ecart"] >= 0]["total_brut_ecart"].mean()) else 0 
253
            neg_total = grouped_compensation[grouped_compensation["total_brut_ecart"] <= 0]["total_brut_ecart"].mean() if pd.notna(grouped_compensation[grouped_compensation["total_brut_ecart"] <= 0]["total_brut_ecart"].mean()) else 0  
254
            
255
            results['repartition_gap'] = {
256
                'name':"repartition_gap",
257
                'data': [
258
                    {"a_market_spread": "Positif", "b_fixed_gross": "{:.2%}".format(pos_fixe), "c_total_gross": "{:.2%}".format(pos_total)},
259
                    {"a_market_spread": "Négatif", "b_fixed_gross": "{:.2%}".format(neg_fixe), "c_total_gross": "{:.2%}".format(neg_total)}
260
                ]
261
            }
262

    
263
        if 'repartition_data_bf' in required_graphs:
264
            
265
            base_var = abs(grouped_compensation["fixe_brut_ecart"]).sum()
266
            #to update
267
            if base_var==0:
268
                base_var=1
269

    
270
            pos_share =  grouped_compensation[grouped_compensation["fixe_brut_ecart"] >= 0]["fixe_brut_ecart"].sum() / base_var if pd.notna(grouped_compensation[grouped_compensation["fixe_brut_ecart"] >= 0]["fixe_brut_ecart"].sum() / base_var) else 0 
271
            neg_share = abs(grouped_compensation[grouped_compensation["fixe_brut_ecart"] <= 0]["fixe_brut_ecart"]).sum() / base_var if pd.notna(abs(grouped_compensation[grouped_compensation["fixe_brut_ecart"] <= 0]["fixe_brut_ecart"]).sum() / base_var) else 0 
272
            
273
            results['repartition_data_bf'] = {
274
                'name': 'repartition_data_bf',
275
                    'data': [
276
                    {"gap_type": "Positif", "positions_percentage": "{:.2%}".format(pos_share)},
277
                    {"gap_type": "Négatif", "positions_percentage": "{:.2%}".format(neg_share)}
278
                ]
279
                }
280

    
281
        if 'repartition_data_bt' in required_graphs:
282

    
283
            total_var = abs(grouped_compensation["total_brut_ecart"]).sum()
284
            if total_var==0:
285
                total_var=1
286

    
287
            pos_share = grouped_compensation[grouped_compensation["total_brut_ecart"] >= 0]["total_brut_ecart"].sum() / total_var if pd.notna(grouped_compensation[grouped_compensation["total_brut_ecart"] >= 0]["total_brut_ecart"].sum() / total_var) else 0
288

    
289
            neg_share = abs(grouped_compensation[grouped_compensation["total_brut_ecart"] <= 0]["total_brut_ecart"]).sum() / total_var if pd.notna(abs(grouped_compensation[grouped_compensation["total_brut_ecart"] <= 0]["total_brut_ecart"]).sum() / total_var) else 0
290
            
291
            results['repartition_data_bt'] = {
292
                'name':'repartition_data_bt',
293
                'data': [
294
                    {"gap_type": "Positif", "positions_percentage": "{:.2%}".format(pos_share)},
295
                    {"gap_type": "Négatif", "positions_percentage": "{:.2%}".format(neg_share)}
296
                ]
297
            }
298
        # Return only requested graphs (with error handling for invalid codes)
299
        return [
300
            (results.get(code, f"{code} is unknown")) 
301
            for code in specifique_graphcode]
302
        
303
    def create_best_matchg_df(df, companyId,condition,colunm):
304
        l=[[ "function_code","subfunction_code", "collaborative_path", "grade"],
305
        [ "function_code","subfunction_code", "grade"],
306
        [ "function_code", "grade"],
307
        [ "grade"]]
308
        n=len(condition)
309
        match=[]
310
        for i in range((len(l)-n),len(l)):
311
            df['match']=df[l[i]].apply(tuple, axis=1)
312
            df_company=df[df["companyId"] == companyId]
313
            if len(df_company)==0:
314
                break
315

    
316
            df_other=df[df["companyId"] != companyId]
317
            
318
            match_df=df_other[df_other['match'].isin(df_company['match'])]
319
                    
320
            Founded = df_company[df_company['match'].isin(match_df['match'])]
321
            matched_data = df[df['match'].isin(match_df['match'])]
322
            
323
            df = df.drop(Founded.index)
324
            match.append(matched_data)
325
        
326
        if len(df_company)!=0:
327
            
328
            df_company=df[df['companyId'] == companyId]
329
        
330
        match.append(df_company)
331

    
332
        best_match_df = pd.concat(match, ignore_index=True)
333
        
334
        return best_match_df
335

    
336
    def buildComparisonDatasets(kpis, company_df, condition1):
337
        base_salary_your_data = company_df.groupby(condition1)['base_salary'].median()
338
        base_salary_your_data = pd.DataFrame(base_salary_your_data.reset_index().rename(columns={"base_salary": "base_salary_your_data"}))
339

    
340
        total_compensation_your_data = company_df.groupby(condition1)['total_compensation'].median()
341
        total_compensation_your_data = pd.DataFrame(total_compensation_your_data.reset_index().rename(columns={"total_compensation": "total_compensation_your_data"}))
342

    
343
        global_compensation_your_data = company_df.groupby(condition1)['global_compensation'].median()
344
        global_compensation_your_data = pd.DataFrame(global_compensation_your_data.reset_index().rename(columns={"global_compensation": "global_compensation_your_data"}))
345

    
346
        res = pd.concat([base_salary_your_data, total_compensation_your_data, global_compensation_your_data], axis=1)
347
        res = res.loc[:, ~res.columns.duplicated()]
348

    
349
        res = pd.merge(kpis, res, on=condition1, how="left")
350

    
351
        base_salary_diff = CompensationsService.calculate_base_salary_diff("base_salary", res)
352
        total_compensation_diff = CompensationsService.calculate_base_salary_diff("total_compensation", res)
353
        global_compensation_diff = CompensationsService.calculate_base_salary_diff("global_compensation", res)
354

    
355
        CompareKPIs = pd.concat([res, base_salary_diff, total_compensation_diff, global_compensation_diff], axis=1)
356
        CompareKPIs = CompareKPIs.loc[:, ~CompareKPIs.columns.duplicated()].replace(np.nan, 0)
357
        return CompareKPIs
358
            
359
    def buildKpisDatasets(compensations_extraction,include,condition,companyId):
360
        if include == False:
361
            compensations = compensations_extraction[compensations_extraction["companyId"] != companyId]
362
        else:
363
            compensations = compensations_extraction
364
        base_salary = compensations.groupby(condition).apply(lambda group:CompensationsService.calculate_salary_statistics(group, "base_salary")).reset_index()
365
        base_salary = pd.concat([base_salary[condition], pd.json_normalize(base_salary[base_salary.columns[-1]])], axis=1)
366

    
367
        total_compensation = compensations.groupby(condition).apply(lambda group: CompensationsService.calculate_salary_statistics(group, "total_compensation")).reset_index()
368
        total_compensation = pd.concat([total_compensation[condition], pd.json_normalize(total_compensation[total_compensation.columns[-1]])], axis=1)
369

    
370
        global_compensation = compensations.groupby(condition).apply(lambda group: CompensationsService.calculate_salary_statistics(group, "global_compensation")).reset_index()
371
        global_compensation = pd.concat([global_compensation[condition], pd.json_normalize(global_compensation[global_compensation.columns[-1]])], axis=1)
372

    
373
        kpis = pd.concat([base_salary, total_compensation, global_compensation], axis=1)
374
        kpis = kpis.loc[:, ~kpis.columns.duplicated()]
375
        return kpis
376
    
377
    #Data loading
378
    @staticmethod
379
    def get_compensation_output_by_criteria(compensations_extraction, companyId, criterias, round ,include,compare):
380
          
381
        Output = []
382
        for criteria in criterias:
383

    
384
            cond = criteria.get("condition")
385
            specifique_graphcode = criteria.get("graph")
386
            column_associe_condition = criteria.get("column_associe_condition")
387
            column_other_display = criteria.get("column_other_display")
388
            best_matching=criteria.get("best_matching")
389
            
390
            # create best matching dataset
391
            if best_matching ==True:
392
                compensations_extraction=CompensationsService.create_best_match_df(compensations_extraction, companyId,cond,column_associe_condition)
393
                condition=['match']
394
            else:
395
                condition=cond
396
            
397
            # select company data
398
            company_df=compensations_extraction[compensations_extraction["companyId"] == companyId]
399

    
400
            #get graphics by criteria
401
            specifique_graph_output = CompensationsService.get_compensation_graph_by_criteria(specifique_graphcode, condition, companyId, compensations_extraction, round)
402
            
403
            #calculate standard kpis
404
            KPIs_Result=CompensationsService.buildKpisDatasets(compensations_extraction,include,condition,companyId)
405

    
406
            # define comparaison condition
407
            # if column_associe_condition not empty, we will use it instead of criteria, add column_associe_condition to the output
408
            if column_associe_condition == []:
409

    
410
                condition1=condition
411
            else:
412
                condition1=column_associe_condition
413
                result[condition1[0]]=pd.merge(KPIs_Result, company_df.groupby(condition)[condition1[0]].unique(), on=condition, how="right")[condition1[0]]
414
                result=result.dropna(subset=[condition1[0]])
415
                result=result.explode(condition1[0]).reset_index(drop=True)
416
            
417
            if compare == True:
418
                result=CompensationsService.buildComparisonDatasets(result, company_df, condition1)
419
            
420
            # add criteria column to the output 
421
            if (best_matching == True) or (column_associe_condition != []):
422
                result= pd.merge(result,company_df[cond+condition], on=condition, how="right")  
423
            else:
424
                pass
425
            
426
            # number columns are calculated based on possible match for the best matching case else on criteria.
427
            if best_matching ==True:
428
                grouping=condition
429
            else:
430
                grouping=condition1
431
        
432
            result ["number_organisation"]=pd.merge(result, compensations_extraction.groupby(grouping)['companyId'].nunique().reset_index(), on=grouping, how="left")['companyId']
433
            result ["number_employee_panel"]=pd.merge(result, compensations_extraction.groupby(grouping)['base_salary'].count().reset_index(), on=grouping, how="left")['base_salary']
434
            result ["number_employee_company"]=pd.merge(result, company_df.groupby(grouping)['base_salary'].count().reset_index(), on=grouping, how="left")['base_salary'].replace(np.nan, 0)
435
            result = result[result["number_employee_panel"] > result["number_employee_company"]]
436
            result = result[result["number_employee_panel"] > 1]
437
            result =  result[result[cond+condition1].apply(tuple, axis=1).isin(company_df[cond+condition1].apply(tuple, axis=1))]
438
            
439
            #add row not founded in the best match to the final output
440
            if best_matching == True:
441
                unfounded=company_df[~company_df[column_associe_condition[0]].isin(result[column_associe_condition[0]])][column_associe_condition[0]]
442
                result=pd.concat([result, unfounded], axis=0).replace(np.nan, 0)
443
            else:
444
                pass
445
            
446
            '''result = result.drop(["number_organisation","number_employee_panel","number_employee_company"], axis=1)'''
447
            
448
            #calculate display variables
449
            if column_other_display!=[]:
450
                display_var= CompensationsService.calculate_display_var(column_other_display,company_df,condition1)
451
                result = pd.merge(result, display_var, on=condition1, how="left")
452
            else:
453
                pass
454
        
455
            result = result.drop_duplicates()
456
            
457
            #formatting output by criteria
458
            result = result.rename(columns={col: f"a_{col}" for col in cond+condition1+column_other_display})
459
            prefixed_condition_cols = [f"a_{col}" for col in cond]
460
            float_cols =[
461
            'base_salary_average', 'base_salary_25', 'base_salary_50', 'base_salary_75',
462
            'base_salary_std_dev', 'base_salary_your_data',
463
            'base_salary_diff_25_perc', 'base_salary_diff_50_perc', 'base_salary_diff_75_perc',
464

    
465
            'total_compensation_average', 'total_compensation_25', 'total_compensation_50',
466
            'total_compensation_75', 'total_compensation_std_dev', 'total_compensation_your_data',
467
            'total_compensation_diff_25_perc', 'total_compensation_diff_50_perc', 'total_compensation_diff_75_perc',
468

    
469
            'global_compensation_average', 'global_compensation_25', 'global_compensation_50',
470
            'global_compensation_75', 'global_compensation_std_dev', 'global_compensation_your_data',
471
            'global_compensation_diff_25_perc', 'global_compensation_diff_50_perc', 'global_compensation_diff_75_perc'
472
            ]
473
            
474
            desired_order = [
475
                'a_matricule',
476
            'a_function_code', 'a_subfunction_code', 'a_collaborative_path', 'a_grade','match',
477
            'a_country', 'a_city', 'a_site', 'a_department','a_job', 'a_function','a_internal_grade', 'a_gender',
478
            'number_organisation','number_employee_panel','number_employee_company'] + float_cols
479
            int_col = [ 'a_grade','number_organisation','number_employee_panel','number_employee_company']
480
            int_col = [col for col in int_col if col in result.columns]
481
            # Reorder columns - only include columns that exist in the result
482
            existing_columns = [col for col in desired_order if col in result.columns]
483
            result = result[existing_columns]
484
            result[int_col] = result[int_col].applymap(lambda x: f"{x:,.{0}f}".replace(",", " "))
485
            float_columns = [col for col in float_cols if col in result.columns]
486
            result[float_columns] = result[float_columns].applymap(lambda x: f"{x:,.{round}f}".replace(",", " "))
487
            # Fill numeric columns with 0
488
            result[result.select_dtypes(include='number').columns] = \
489
                result.select_dtypes(include='number').fillna(0)
490
            # Fill object/string columns with 'Missing'
491
            result[result.select_dtypes(include='object').columns] = \
492
                result.select_dtypes(include='object').fillna('Missing')
493

    
494
            output = {
495
                "criteria": prefixed_condition_cols,
496
                "table_data": result.to_dict(orient="records"),
497
                "graph_data": specifique_graph_output
498
            }
499
            Output.append(output)
500

    
501
        return Output
502
    
503
    def BuildFinalResponse(general_graphcode, filters, companyId, criteria, round, include, compare):
504
        compensations_extraction = CompensationsService.get_specifique_compensations(filters, companyId)
505
        
506
        if compensations_extraction.empty:
507
            return "No compensations data"
508
        else:
509
            compensations_extraction.rename(columns={"gross_annual_salary": "base_salary"}, inplace=True)
510
            compensations_extraction["total_compensation"] = compensations_extraction[['base_salary', 'total_amount_performance_bonus_awarded',
511
                                                                        'total_amount_sales_bonus_awarded', 'total_amount_sales_commissions_awarded']].sum(axis=1)
512

    
513
            compensations_extraction["global_compensation"] = compensations_extraction[['base_salary', 'meal_allowance', 'location_allowance', 'transportation_allowance','other_bonuses_awarded_last_year', 'fixed_allowances', 'total_amount_performance_bonus_awarded',
514
                                                                        'total_amount_sales_bonus_awarded', 'total_amount_sales_commissions_awarded','schooling_allowance', 'seniority_bonus']].sum(axis=1)
515

    
516
        output_by_criteria=CompensationsService.get_compensation_output_by_criteria(compensations_extraction, companyId, criteria, round ,include,compare)
517

    
518
        graphs = CompensationsService.get_compensation_general_graph(general_graphcode, companyId, compensations_extraction, round)
519

    
520
        final_output = {"graphs": graphs, "tables": output_by_criteria}
521
        
522
        return final_output
(3-3/3)