Skip to content

Commit f5e737f

Browse files
committed
MCDM
1 parent 95fdf94 commit f5e737f

9 files changed

Lines changed: 3943 additions & 0 deletions

File tree

climada/engine/option_appraisal/MCDM/DecisionMatrix.py

Lines changed: 1251 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
# I want to make a data frame container class that stores the following four data frames ranks_df, ranks_crit_df, ranks_MCDM_df, alt_exc_nan_df, alt_exc_const_df and has the following methods:
2+
3+
4+
import matplotlib.pyplot as plt
5+
import numpy as np
6+
7+
# Importing the libraries
8+
import pandas as pd
9+
from tabulate import tabulate
10+
11+
from .utils import filter_dataframe
12+
13+
14+
# make a class
15+
class RanksOutput:
16+
def __init__(
17+
self,
18+
ranks_df,
19+
ranks_crit_df,
20+
ranks_MCDM_df,
21+
alt_exc_nan_df,
22+
alt_exc_const_df,
23+
mcdm_cols,
24+
comp_rank_cols,
25+
dm,
26+
):
27+
self.ranks_df = ranks_df
28+
self.ranks_crit_df = ranks_crit_df
29+
self.ranks_MCDM_df = ranks_MCDM_df
30+
self.alt_exc_nan_df = alt_exc_nan_df
31+
self.alt_exc_const_df = alt_exc_const_df
32+
33+
self.mcdm_cols = mcdm_cols
34+
self.comp_rank_cols = comp_rank_cols
35+
36+
self.dm = dm
37+
38+
# Check if self.dm.unc_smpls_df is not None
39+
if isinstance(self.dm.unc_smpls_df, pd.DataFrame):
40+
self.counts_rank_df, self.rel_counts_rank_df = calculate_counts(
41+
self.dm.crit_cols,
42+
mcdm_cols,
43+
comp_rank_cols,
44+
self.dm.unc_smpls_df,
45+
ranks_df,
46+
)
47+
else:
48+
self.counts_rank_df = None
49+
self.rel_counts_rank_df = None
50+
51+
# make a method to plot the ranks
52+
def plot_ranks(
53+
self,
54+
rank_type="MCDM",
55+
alt_name_col="Alternative ID",
56+
disp_rnk_cols=[],
57+
sort_by_col=None,
58+
transpose=False,
59+
group_id="G1",
60+
state_id="S1",
61+
):
62+
63+
# Get the disp_rnk_cols
64+
if disp_rnk_cols:
65+
legend_title = "Rank columns"
66+
df = self.ranks_df
67+
elif rank_type == "criteria":
68+
legend_title = "Criteria"
69+
df = self.ranks_crit_df
70+
disp_rnk_cols = self.dm.crit_cols
71+
elif rank_type == "MCDM":
72+
legend_title = "MCDM method"
73+
df = self.ranks_MCDM_df
74+
disp_rnk_cols = self.mcdm_cols + self.comp_rank_cols
75+
76+
# Filter out based on group_id and state_id
77+
df = df[df["Group ID"] == group_id]
78+
df = df[df["Sample ID"] == state_id]
79+
80+
# Filter out the columns
81+
df = df[[alt_name_col] + disp_rnk_cols]
82+
83+
# Store number of ranks
84+
step = 1
85+
list_rank = np.arange(1, len(df) + 1, step)
86+
87+
# Sort the columns
88+
if sort_by_col:
89+
df = df.sort_values(by=sort_by_col, ascending=True)
90+
91+
# Check if transpose
92+
if not transpose:
93+
df = df.set_index(alt_name_col)
94+
else:
95+
df = df.set_index(alt_name_col).transpose()
96+
# Rename the index
97+
df.index.name = "Rank columns"
98+
# Rename the legend title
99+
legend_title = alt_name_col
100+
101+
# Plot the dataframe
102+
ax = df.plot(
103+
kind="bar", width=0.8, stacked=False, edgecolor="black", figsize=(15, 8)
104+
)
105+
ax.set_xlabel(df.index.name, fontsize=12)
106+
ax.set_ylabel("Rank", fontsize=12)
107+
ax.set_yticks(list_rank)
108+
109+
# Make rotation of the labels tilted 45 degrees and truncate to the first 10 characters
110+
ax.set_xticklabels([label[:10] for label in df.index], rotation=45)
111+
ax.tick_params(axis="both", labelsize=12)
112+
y_ticks = ax.yaxis.get_major_ticks()
113+
ax.set_ylim(0, len(list_rank) + 1)
114+
115+
# Legend
116+
plt.legend(
117+
bbox_to_anchor=(0.0, 1.02, 1.0, 0.102),
118+
loc="lower left",
119+
ncol=4,
120+
mode="expand",
121+
borderaxespad=0.0,
122+
edgecolor="black",
123+
fontsize=12,
124+
title=legend_title,
125+
)
126+
127+
ax.grid(True, linestyle=":")
128+
ax.set_axisbelow(True)
129+
plt.tight_layout()
130+
plt.show()
131+
132+
# make a print function
133+
def print_rankings(
134+
self, disp_filt={}, disp_rnk_cols=[], rank_type="MCDM", sort_by_col=None
135+
):
136+
137+
# Filter the rank columns
138+
filt_rank_df = filter_dataframe(self.ranks_df, disp_filt)[0]
139+
140+
# Get the disp_rnk_cols
141+
if disp_rnk_cols:
142+
pass
143+
elif rank_type == "criteria":
144+
disp_rnk_cols = self.dm.crit_cols
145+
elif rank_type == "MCDM":
146+
disp_rnk_cols = self.mcdm_cols + self.comp_rank_cols
147+
148+
# Define base column
149+
base_cols = list(self.dm.alternatives_df.columns) + ["Group ID", "Sample ID"]
150+
if isinstance(self.dm.groups_df, pd.DataFrame):
151+
base_cols += list(self.dm.groups_df.columns)
152+
if isinstance(self.dm.unc_smpls_df, pd.DataFrame):
153+
base_cols += list(self.dm.unc_smpls_df.columns)
154+
155+
# Remove duplicates in base_cols
156+
base_cols = list(dict.fromkeys(base_cols))
157+
158+
# Ranking columns to print
159+
filt_rank_df = filt_rank_df[base_cols + disp_rnk_cols]
160+
161+
# Print the rankings per group and state combo
162+
for _, group_scen_df in (
163+
filt_rank_df[["Group ID", "Sample ID"]].drop_duplicates().iterrows()
164+
):
165+
# Print if there are more than one group and state
166+
if (
167+
len(filt_rank_df[["Group ID"]].drop_duplicates()) > 1
168+
and len(filt_rank_df[["Sample ID"]].drop_duplicates()) > 1
169+
):
170+
group_id = group_scen_df["Group ID"]
171+
scen_id = group_scen_df["Sample ID"]
172+
print(f"Group: {group_id}, State: {scen_id}")
173+
print("-----------------------------------")
174+
elif len(filt_rank_df[["Group ID"]].drop_duplicates()) > 1:
175+
group_id = group_scen_df["Group ID"]
176+
print(f"Group: {group_id}")
177+
print("-----------------------------------")
178+
elif len(filt_rank_df[["Sample ID"]].drop_duplicates()) > 1:
179+
scen_id = group_scen_df["Sample ID"]
180+
print(f"State: {scen_id}")
181+
print("-----------------------------------")
182+
183+
# Filter the group and state
184+
sg_df = filt_rank_df[
185+
filt_rank_df[["Group ID", "Sample ID"]]
186+
.isin(group_scen_df[["Group ID", "Sample ID"]].values)
187+
.all(axis=1)
188+
]
189+
190+
# For the print exclude the group and state columns and the index column and sort by sort_by_col
191+
if sort_by_col:
192+
print_df = sg_df.drop(["Group ID", "Sample ID"], axis=1).sort_values(
193+
by=sort_by_col, ascending=True
194+
)
195+
else:
196+
print_df = sg_df.drop(["Group ID", "Sample ID"], axis=1)
197+
print_df = print_df.set_index("Alternative ID")
198+
print(tabulate(print_df, headers="keys", tablefmt="psql"))
199+
print("\n")
200+
201+
def plot_rank_distribution(
202+
self, disp_rnk_col, alt_name_col="Alternative ID", sort_by_perf=True
203+
):
204+
# Assuming df is your DataFrame and it's already been prepared as needed
205+
pivot_df = self.rel_counts_rank_df.pivot(
206+
index=alt_name_col, columns="Rank_Count", values=disp_rnk_col
207+
)
208+
209+
# Move column with 0 rank to the end
210+
pivot_df = pivot_df[[col for col in pivot_df.columns if col != 0] + [0]]
211+
# Rename the column to null
212+
pivot_df.rename(columns={0: "null"}, inplace=True)
213+
214+
# Normalize the data to get percentages and multiply by 100
215+
pivot_df = pivot_df.div(pivot_df.sum(axis=1), axis=0) * 100
216+
if sort_by_perf:
217+
# Calculate the mean for each alternative based on multplying the column value with the cell calue for each row
218+
# exclude the last column which is the 0 rank
219+
pivot_df["mean"] = pivot_df.apply(
220+
lambda row: np.mean(row[:-1] * pivot_df.columns[:-1]), axis=1
221+
)
222+
# Sort the pivoted data frame based on the sorted_cum_sum_df
223+
sorted_pivot_df = pivot_df.sort_values(by="mean", ascending=True).drop(
224+
"mean", axis=1
225+
)
226+
227+
# Create a colormap
228+
cmap = plt.get_cmap("plasma") # Changed to a more contrasting colormap
229+
colors = cmap(np.linspace(0, 1, len(pivot_df.columns)))
230+
231+
# Plot the DataFrame
232+
ax = sorted_pivot_df.plot(
233+
kind="bar", stacked=True, figsize=(15, 10), color=colors
234+
) # Increased figure size
235+
236+
plt.title("Distribution of Ranking Results", fontsize=20)
237+
plt.xlabel(alt_name_col, fontsize=16)
238+
plt.ylabel("Percentage of Total Samples", fontsize=16)
239+
240+
# Move legend to the left side and increase its size
241+
plt.legend(
242+
loc="center left", bbox_to_anchor=(1, 0.5), prop={"size": 14}, title="Rank"
243+
)
244+
245+
# Loop through the bars to annotate each segment with Rank_Count
246+
for bar in ax.containers:
247+
for rect in bar:
248+
# Calculate height and width for the annotation position
249+
height = rect.get_height()
250+
width = rect.get_width()
251+
x = rect.get_x()
252+
y = rect.get_y()
253+
254+
# The label is the Rank_Count, which corresponds to the column names in pivot_df
255+
# We identify the Rank_Count based on the rectangle's position and size
256+
label = bar.get_label()
257+
258+
# Only annotate if there's enough space (height) in the bar segment
259+
if height > 0:
260+
ax.text(
261+
x + width / 2,
262+
y + height / 2,
263+
str(label),
264+
ha="center",
265+
va="center",
266+
color="white",
267+
fontsize=12,
268+
) # Changed text color to white for better visibility
269+
270+
# Tilt the x-axis labels
271+
plt.xticks(rotation=45)
272+
273+
plt.show()
274+
275+
276+
def calculate_counts(crit_cols, mcdm_cols, comp_rank_cols, unc_smpls_df, ranks_df):
277+
# Calculate max rank value and create base rank count DataFrame
278+
max_rank_value = ranks_df[crit_cols + mcdm_cols + comp_rank_cols].max().max()
279+
base_rank_count_df = pd.DataFrame(
280+
{"Rank_Count": range(max_rank_value + 1), "merge_": 1}
281+
)
282+
283+
# Calculate max rank value and create base rank count DataFrame
284+
max_rank_value = ranks_df[crit_cols + mcdm_cols + comp_rank_cols].max().max()
285+
base_rank_count_df = pd.DataFrame(
286+
{"Rank_Count": range(max_rank_value + 1), "merge_": 1}
287+
)
288+
289+
# Define columns
290+
rank_cols = crit_cols + mcdm_cols + comp_rank_cols
291+
base_cols = [
292+
col
293+
for col in ranks_df.columns
294+
if col not in rank_cols + list(unc_smpls_df.columns)
295+
]
296+
297+
# Initialize result DataFrames
298+
all_count_ranks_df, all_rel_counts_df = pd.DataFrame(), pd.DataFrame()
299+
300+
# Iterate through all unique 'Group ID's
301+
for _, group_df in ranks_df[["Group ID"]].drop_duplicates().iterrows():
302+
sg_df = ranks_df[
303+
ranks_df[["Group ID"]].isin(group_df[["Group ID"]].values).all(axis=1)
304+
]
305+
306+
# Prepare counts DataFrame
307+
counts_df = sg_df[base_cols].copy().drop_duplicates()
308+
counts_df = pd.merge(
309+
base_rank_count_df, counts_df.assign(merge_=1), on="merge_"
310+
).drop("merge_", axis=1)
311+
counts_df[crit_cols + mcdm_cols + comp_rank_cols] = 0
312+
313+
# Count the relative number of ranks for each alternative and store in ranks_count_df
314+
for rank_count in range(max_rank_value + 1):
315+
for alt in sg_df["Alternative ID"].unique():
316+
for col in rank_cols:
317+
count = (
318+
(sg_df[col] == rank_count) & (sg_df["Alternative ID"] == alt)
319+
).sum()
320+
row_idx = (counts_df["Rank_Count"] == rank_count) & (
321+
counts_df["Alternative ID"] == alt
322+
)
323+
counts_df.loc[row_idx, col] = count
324+
325+
# Append counts_df to count_ranks_df
326+
all_count_ranks_df = pd.concat([all_count_ranks_df, counts_df])
327+
328+
# Calculate the relative counts
329+
rel_counts_df = counts_df.copy()
330+
rel_counts_df[rank_cols] = rel_counts_df[rank_cols] / len(
331+
sg_df["Sample ID"].unique()
332+
)
333+
334+
# Append rel_counts_df to all_rel_counts_df
335+
all_rel_counts_df = pd.concat([all_rel_counts_df, rel_counts_df])
336+
337+
return all_count_ranks_df, all_rel_counts_df

climada/engine/option_appraisal/MCDM/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)