espejelomar commited on
Commit
0472866
1 Parent(s): 20b860f

Upload folder using huggingface_hub

Browse files
data/figures/developer_engagement_journey_2024-03-04.png ADDED
data/figures/developer_survival_curve_2024-03-04.png ADDED
data/source/all_networks_developer_classification.csv CHANGED
The diff for this file is too large to render. See raw diff
 
debug.csv ADDED
The diff for this file is too large to render. See raw diff
 
github_metrics/__pycache__/utils.cpython-311.pyc ADDED
Binary file (1.7 kB). View file
 
github_metrics/developer_survival_plot.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+ import pandas as pd
4
+ import seaborn as sns
5
+ from lifelines import KaplanMeierFitter
6
+ from matplotlib.colors import LinearSegmentedColormap
7
+
8
+ from utils import save_plot
9
+
10
+
11
+ def load_and_prepare_data(file_path):
12
+ """
13
+ Load CSV data, convert 'month_year' to datetime, and prepare cohort and duration calculations.
14
+ Filter data to include only entries from 2021 onwards and adjust the cohort calculation based on the first active month.
15
+ Additionally, eliminate all months with a negative 'Order' so we only get the months after the cohort of the individual.
16
+ """
17
+ df = pd.read_csv(file_path)
18
+ df["month_year"] = pd.to_datetime(df["month_year"], format="%B_%Y")
19
+ df = df[df["month_year"] >= "2021-09-01"]
20
+ df["Active"] = df["total_commits"] > 0
21
+ df.sort_values(by=["developer", "month_year"], inplace=True)
22
+
23
+ first_active_month = (
24
+ df[df["Active"]].groupby("developer")["month_year"].min().reset_index()
25
+ )
26
+ first_active_month.rename(columns={"month_year": "FirstActiveMonth"}, inplace=True)
27
+
28
+ df = df.merge(first_active_month, on="developer", how="left")
29
+
30
+ df["Cohort"] = df["FirstActiveMonth"].dt.to_period("M")
31
+
32
+ def calculate_order(row):
33
+ if pd.isnull(row["Cohort"]):
34
+ return None
35
+ return (row["month_year"].to_period("M") - row["Cohort"]).n
36
+
37
+ df["Order"] = df.apply(calculate_order, axis=1)
38
+
39
+ df = df[df["Order"] >= 0]
40
+ df["Inactive_Month"] = df.groupby("developer")["Active"].transform(
41
+ lambda x: x.rolling(window=2, min_periods=2).sum() == 0
42
+ )
43
+ df["inactive_for_two_months"] = (
44
+ df.groupby("developer")["Inactive_Month"].transform("max").astype(int)
45
+ )
46
+
47
+ df["duration"] = df.groupby("developer")["month_year"].transform("nunique")
48
+ df.to_csv("debug.csv", index=False)
49
+
50
+ return df
51
+
52
+
53
+ def visualize_developer_retention(df):
54
+ cohort_counts = (
55
+ df[~df["Inactive_Month"]]
56
+ .groupby(["Cohort", "Order"])
57
+ .developer.nunique()
58
+ .unstack(0)
59
+ )
60
+
61
+ cohort_sizes = cohort_counts.iloc[0]
62
+ retention = cohort_counts.divide(cohort_sizes, axis=1)
63
+
64
+ colors = [(0, "#FF0000"), (0.15, "#FFA500"), (0.2, "#FFFF00"), (1, "#008000")]
65
+ cmap = LinearSegmentedColormap.from_list("custom_cmap", colors, N=256)
66
+ plt.figure(figsize=(12, 8)) # Adjusted figure size for better visibility
67
+ sns.heatmap(retention.T, annot=False, cmap=cmap)
68
+ plt.title("Journey Through Code: Tracking Developer Engagement Over Time", pad=20)
69
+
70
+ plt.subplots_adjust(bottom=0.3)
71
+
72
+ description_text = (
73
+ "This heatmap visualizes the engagement journey of developers, tracked monthly across cohorts."
74
+ " Each cohort represents developers who began contributing in the same month."
75
+ " The color gradient from red to green signifies the evolution of active engagement over time,"
76
+ " with red indicating lower engagement levels and green denoting higher activity."
77
+ " Cohorts are plotted on the y-axis, and the actual months since the start of the cohort on the x-axis."
78
+ " This visualization offers insights into how developer activity trends evolve,"
79
+ " highlighting periods of increased or decreased engagement and aiding in understanding"
80
+ " the effectiveness of retention strategies over time."
81
+ " Parameters:"
82
+ "(a) A developer is considered inactive if they have at least 2 continuous inactive months."
83
+ "(b) With one commit in a month, the developer is considered active."
84
+ "(c) The data is filtered to include only entries from September 2021 onwards."
85
+ )
86
+ plt.figtext(0.5, -0.0001, description_text, ha="center", fontsize=9, wrap=True)
87
+
88
+ save_plot(plt, "developer_engagement_journey")
89
+
90
+
91
+ def survival_curve_analysis_and_plot(df):
92
+ """
93
+ Perform analysis on the DataFrame to calculate durations and generate visualizations, with annotations explaining the analysis.
94
+ Adjust the event definition and perform Log-Rank Test.
95
+ """
96
+ summary_df = (
97
+ df.groupby("developer")
98
+ .agg({"duration": "first", "inactive_for_two_months": "last"})
99
+ .reset_index()
100
+ )
101
+
102
+ kmf = KaplanMeierFitter()
103
+ kmf.fit(
104
+ durations=summary_df["duration"],
105
+ event_observed=summary_df["inactive_for_two_months"],
106
+ label="Developer Survival Probability",
107
+ )
108
+
109
+ plt.figure(figsize=(10, 6))
110
+ ax = plt.subplot(111)
111
+ kmf.plot_survival_function(ax=ax)
112
+
113
+ plt.title("Developer Survival Curve: Probability of Active Contribution Over Time")
114
+ plt.grid(True, which="both", linestyle="--", linewidth=0.5)
115
+ median_survival_time = kmf.median_survival_time_
116
+ ax.axhline(y=0.5, color="red", linestyle="--")
117
+ ax.text(
118
+ median_survival_time,
119
+ 0.48,
120
+ "Median Survival Time",
121
+ verticalalignment="center",
122
+ color="red",
123
+ fontsize=8,
124
+ )
125
+ ax.axvline(x=3, color="green", linestyle="--")
126
+ ax.text(
127
+ 3,
128
+ 0.95,
129
+ "Inactive Month + 1",
130
+ verticalalignment="top",
131
+ horizontalalignment="center",
132
+ color="green",
133
+ fontsize=8,
134
+ )
135
+ ax.axvline(x=median_survival_time, color="green", linestyle="--")
136
+ ax.text(
137
+ len(df["duration"].unique()),
138
+ 0.9,
139
+ f"After month {int(median_survival_time)} the probability of developers staying is lower than 50 percent",
140
+ verticalalignment="top",
141
+ horizontalalignment="right",
142
+ color="green",
143
+ fontsize=8,
144
+ )
145
+ ax.set_yticks(np.arange(0, 1.1, 0.1))
146
+
147
+ # Setting the x-axis and y-axis labels as per the request
148
+ plt.xlabel("Months since the developer started committing code")
149
+ plt.ylabel("Probability of a developer staying in the ecosystem")
150
+
151
+ description_text = (
152
+ "The Kaplan-Meier survival curve shows the probability of developers continuing to contribute over time."
153
+ "Parameters:"
154
+ "(a) A developer is consider as inactive if they have at least 2 continuous inactive months."
155
+ "(b) With one commit in a month, the developer is considered active."
156
+ "(c) The data is filtered to include only entries from September 2021 onwards."
157
+ "The Kaplan-Meier estimator is a non-parametric statistic used to estimate the survival function from lifetime data."
158
+ "It requires to know the duration each subject was observed for, and whether the event of interest"
159
+ "(in this case, becoming inactive for two months) was observed."
160
+ "The 'Median Survival Time' shows when the chance of further contributions drops below 50%. "
161
+ "This analysis helps in understanding the retention of developers and predicting future contribution patterns."
162
+ )
163
+ plt.figtext(0.1, -0.1, description_text, ha="left", fontsize=8, wrap=True)
164
+
165
+ save_plot(plt, "developer_survival_curve")
166
+
167
+
168
+ if __name__ == "__main__":
169
+ csv_path = "data/source/all_networks_developer_classification.csv"
170
+ df = load_and_prepare_data(csv_path)
171
+
172
+ visualize_developer_retention(df)
173
+
174
+ survival_curve_analysis_and_plot(df)
github_metrics/main.py CHANGED
@@ -1,112 +1,370 @@
1
  import gradio as gr
2
  import pandas as pd
3
- import matplotlib.pyplot as plt
4
- from io import StringIO
5
  from termcolor import colored
 
 
6
 
7
- # Load the dataset with debug prints
8
- def load_dataset():
9
- try:
10
- print(colored("Loading dataset...", "blue"))
11
- df = pd.read_csv("data/source/all_networks_developer_classification.csv")
12
- # Ensure the month_year column is in the correct datetime format
13
- df['month_year'] = pd.to_datetime(df['month_year'], format='%B_%Y') # Adjust format if necessary
14
- return df
15
- except Exception as e:
16
- print(colored(f"Error loading dataset: {e}", "red"))
17
- raise
18
-
19
- # Process input and generate plot and classification with debug prints
20
- def process_input(input_text, uploaded_file):
21
  try:
22
  print(colored("Processing input...", "blue"))
23
-
24
- # Check if a file was uploaded
25
  if uploaded_file is not None:
26
  print(colored("Reading from uploaded file...", "blue"))
27
- # Decode the bytes object to string
28
  file_content = uploaded_file.decode("utf-8")
29
- # Split the content by newlines and strip each handle
30
- github_handles = [handle.strip() for handle in file_content.split('\n') if handle.strip()]
31
  else:
32
  github_handles = [handle.strip() for handle in input_text.split(",")]
33
-
34
  print(colored(f"GitHub handles: {github_handles}", "blue"))
35
 
36
- # Load dataset
37
- df = load_dataset()
38
-
39
- # Filter dataset for the provided GitHub handles
40
  print(colored("Filtering dataset...", "blue"))
41
- filtered_df = df[df['developer'].isin(github_handles)]
42
-
43
- # Generate plot
44
- print(colored("Generating plot...", "blue"))
45
- fig, ax = plt.subplots()
46
- for handle in github_handles:
47
- dev_df = filtered_df[filtered_df['developer'] == handle]
48
- dev_df = dev_df.sort_values('month_year')
49
- ax.plot(dev_df['month_year'], dev_df['total_commits'], label=handle)
50
-
51
- ax.set_xlabel("Month")
52
- ax.set_ylabel("Number of Commits")
53
- ax.legend()
54
- plt.xticks(rotation=45)
55
- plt.tight_layout()
56
-
57
- # Generate classification table
 
 
58
  print(colored("Classifying developers...", "blue"))
59
- classification = []
60
- for handle in github_handles:
61
- dev_df = filtered_df[filtered_df['developer'] == handle]
62
- last_3_months = pd.Timestamp.now() - pd.DateOffset(months=3)
63
- recent_activity = dev_df[dev_df['month_year'] >= last_3_months]
64
- total_recent_commits = recent_activity['total_commits'].sum()
65
-
66
- if dev_df.empty:
67
- status = "Always been inactive"
68
- elif recent_activity.empty:
69
- status = "Previously active but no longer"
70
- elif total_recent_commits < 20:
71
- status = "Low-level active"
72
- else:
73
- status = "Highly involved"
74
-
75
- classification.append((handle, status))
76
-
77
- classification_df = pd.DataFrame(classification, columns=["Developer", "Classification"]).sort_values("Classification", ascending=False)
78
  print(colored("Classification completed.", "blue"))
79
 
80
- # Return plot and classification table
81
- return fig, classification_df
 
 
 
 
82
  except Exception as e:
83
  print(colored(f"Error processing input: {e}", "red"))
84
- raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
- # Gradio interface with descriptions and debug prints
87
  with gr.Blocks() as app:
88
- gr.Markdown("## GitHub Starknet Developer Insights")
89
- gr.Markdown("""
90
- This tool allows you to analyze the GitHub activity of developers within the Starknet ecosystem.
91
- Enter GitHub handles separated by commas or upload a CSV file with GitHub handles in a single column
92
- to see their monthly commit activity and their current involvement classification.
93
- """)
94
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  with gr.Row():
96
- text_input = gr.Textbox(label="Enter GitHub handles separated by commas", placeholder="e.g., user1,user2,user3")
97
- file_input = gr.File(label="Or upload a CSV file with GitHub handles in a single column", type="binary")
98
- gr.Markdown("""
99
- *Note:* When uploading a CSV, ensure it contains a single column of GitHub handles without a header row.
100
- """)
101
- btn = gr.Button("Analyze")
102
- plot_output = gr.Plot(label="Commits per Month")
103
- table_output = gr.Dataframe(label="Developer Classification")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
- btn.click(process_input, inputs=[text_input, file_input], outputs=[plot_output, table_output])
 
 
 
 
 
 
 
 
 
 
106
 
107
- print(colored("Gradio app initialized.", "blue"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
  if __name__ == "__main__":
110
  print(colored("Launching app...", "blue"))
111
- app.launch(share=True)
112
-
 
1
  import gradio as gr
2
  import pandas as pd
3
+ import plotly.express as px
4
+ import plotly.graph_objects as go
5
  from termcolor import colored
6
+ from scipy.stats import mannwhitneyu
7
+ from utils import load_all_developers_dataset
8
 
9
+ def process_input(input_text, uploaded_file, program_end_date=None, event_name=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  try:
11
  print(colored("Processing input...", "blue"))
 
 
12
  if uploaded_file is not None:
13
  print(colored("Reading from uploaded file...", "blue"))
 
14
  file_content = uploaded_file.decode("utf-8")
15
+ github_handles = [handle.strip() for handle in file_content.split("\n") if handle.strip()]
 
16
  else:
17
  github_handles = [handle.strip() for handle in input_text.split(",")]
 
18
  print(colored(f"GitHub handles: {github_handles}", "blue"))
19
 
20
+ df = load_all_developers_dataset()
 
 
 
21
  print(colored("Filtering dataset...", "blue"))
22
+ one_year_ago = pd.Timestamp.now() - pd.DateOffset(years=1)
23
+ filtered_df = df[(df["developer"].isin(github_handles)) & (df["month_year"] >= one_year_ago)]
24
+ filtered_df = filtered_df.sort_values(by=["developer", "month_year"])
25
+ filtered_df.loc[:, "month_year"] = pd.to_datetime(filtered_df["month_year"])
26
+
27
+ line_fig = create_line_plot(filtered_df, github_handles, program_end_date)
28
+ analysis_result = perform_statistical_analysis(filtered_df, github_handles, program_end_date)
29
+ new_developers_count = count_new_developers(filtered_df, github_handles, program_end_date)
30
+
31
+ last_3_months = pd.Timestamp.now() - pd.DateOffset(months=3)
32
+ recent_activity_user = filtered_df[filtered_df["month_year"] >= last_3_months]
33
+ all_devs_df = load_all_developers_dataset()
34
+ all_devs_filtered_df = all_devs_df[(all_devs_df["month_year"] >= last_3_months)]
35
+ other_devs_recent_activity = all_devs_filtered_df[~all_devs_filtered_df["developer"].isin(github_handles)]
36
+
37
+ user_specified_active = recent_activity_user[recent_activity_user["total_commits"] > 0]
38
+ other_developers_active = other_devs_recent_activity[other_devs_recent_activity["total_commits"] > 0]
39
+ box_fig = create_box_plot(user_specified_active, other_developers_active)
40
+
41
  print(colored("Classifying developers...", "blue"))
42
+ classification_df = classify_developers(github_handles, recent_activity_user)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  print(colored("Classification completed.", "blue"))
44
 
45
+ comparison_result = compare_user_developers_to_others(user_specified_active, other_developers_active, df, program_end_date)
46
+ growth_rate_result = compare_growth_rate(user_specified_active, other_developers_active, df)
47
+
48
+ tldr_summary = generate_tldr_summary(github_handles, classification_df, analysis_result, new_developers_count, comparison_result, growth_rate_result, event_name)
49
+
50
+ return line_fig, box_fig, classification_df, analysis_result, new_developers_count, comparison_result, growth_rate_result, tldr_summary
51
  except Exception as e:
52
  print(colored(f"Error processing input: {e}", "red"))
53
+ return None, None, None, None, "Error in processing input.", None, None, "Error in processing input."
54
+
55
+ def create_line_plot(filtered_df, github_handles, program_end_date):
56
+ all_developers = pd.DataFrame({"developer": github_handles, "month_year": pd.Timestamp.now(), "total_commits": 0})
57
+ plot_df = pd.concat([filtered_df, all_developers])
58
+ plot_df = plot_df.groupby(["developer", "month_year"])["total_commits"].sum().reset_index()
59
+ line_fig = px.line(
60
+ plot_df,
61
+ x="month_year",
62
+ y="total_commits",
63
+ color="developer",
64
+ labels={"month_year": "Month", "total_commits": "Number of Commits"},
65
+ title="Commits per Month",
66
+ )
67
+ if program_end_date:
68
+ program_end_date = pd.to_datetime(program_end_date)
69
+ line_fig.add_vline(x=program_end_date, line_width=2, line_dash="dash", line_color="red")
70
+ return line_fig
71
+
72
+ def create_box_plot(user_specified_active, other_developers_active):
73
+ box_fig = go.Figure()
74
+ box_fig.add_trace(go.Box(y=user_specified_active["total_commits"], name="User Specified Developers"))
75
+ box_fig.add_trace(go.Box(y=other_developers_active["total_commits"], name="Other Developers"))
76
+ box_fig.update_layout(
77
+ title="Comparison of Monthly Commits in the Last 3 Months: User Specified vs. Other Developers (Active Only)",
78
+ yaxis_title="Total Monthly Commits",
79
+ yaxis=dict(range=[0, 50]),
80
+ )
81
+ return box_fig
82
+
83
+ def classify_developers(github_handles, recent_activity_user):
84
+ classification = []
85
+ for handle in github_handles:
86
+ dev_df = recent_activity_user[recent_activity_user["developer"] == handle]
87
+ total_recent_commits = dev_df["total_commits"].sum()
88
+ if dev_df.empty or total_recent_commits == 0:
89
+ status = "Always been inactive"
90
+ elif total_recent_commits < 20:
91
+ status = "Low-level active"
92
+ else:
93
+ status = "Highly involved"
94
+ classification.append((handle, status, total_recent_commits))
95
+
96
+ sort_keys = {
97
+ "Highly involved": 1,
98
+ "Low-level active": 2,
99
+ "Previously active but no longer": 3,
100
+ "Always been inactive": 4,
101
+ }
102
+ classification_df = pd.DataFrame(classification, columns=["Developer", "Classification", "Total Recent Commits"])
103
+ classification_df["Sort Key"] = classification_df["Classification"].map(sort_keys)
104
+ classification_df.sort_values(by=["Sort Key", "Total Recent Commits"], ascending=[True, False], inplace=True)
105
+ classification_df.drop(["Sort Key", "Total Recent Commits"], axis=1, inplace=True)
106
+ return classification_df
107
+
108
+ def perform_statistical_analysis(filtered_df, github_handles, program_end_date_str):
109
+ if program_end_date_str is None:
110
+ return "Program end date not provided. Unable to perform statistical analysis."
111
+
112
+ program_end_date = pd.to_datetime(program_end_date_str)
113
+ before_program = filtered_df[filtered_df["month_year"] < program_end_date]
114
+ after_program = filtered_df[filtered_df["month_year"] >= program_end_date]
115
+
116
+ before_counts = before_program.groupby("developer")["total_commits"].median()
117
+ after_counts = after_program.groupby("developer")["total_commits"].median()
118
+
119
+ all_developers = pd.Series(0, index=github_handles)
120
+ before_counts = before_counts.reindex(all_developers.index, fill_value=0)
121
+ after_counts = after_counts.reindex(all_developers.index, fill_value=0)
122
+
123
+ if len(before_counts) < 2 or len(after_counts) < 2:
124
+ return "Not enough data for statistical analysis."
125
+
126
+ stat, p_value = mannwhitneyu(after_counts, before_counts)
127
+ analysis_result = f"Mann-Whitney U test statistic: {stat:.3f}, P-value: {p_value:.3f}\n"
128
+
129
+ if p_value < 0.2:
130
+ if stat > 0:
131
+ analysis_result += "Difference in commit activity before and after the program is considered significant. " \
132
+ "The commit activity is higher after the program."
133
+ else:
134
+ analysis_result += "Difference in commit activity before and after the program is considered significant. " \
135
+ "The commit activity is lower after the program."
136
+ else:
137
+ analysis_result += "No significant difference in commit activity before and after the program."
138
+
139
+ return analysis_result
140
+
141
+ def count_new_developers(filtered_df, github_handles, program_end_date_str):
142
+ if program_end_date_str is None:
143
+ return "Program end date not provided. Unable to count new developers."
144
+
145
+ program_end_date = pd.to_datetime(program_end_date_str)
146
+ two_months_after_program = program_end_date + pd.DateOffset(months=2)
147
+
148
+ before_program = filtered_df[filtered_df["month_year"] < program_end_date]
149
+ after_program = filtered_df[(filtered_df["month_year"] >= program_end_date) & (filtered_df["month_year"] <= two_months_after_program)]
150
+
151
+ before_developers = before_program["developer"].unique()
152
+ after_developers = after_program["developer"].unique()
153
+
154
+ new_developers = set(after_developers) - set(before_developers)
155
+ new_developers_str = ", ".join(new_developers)
156
+
157
+ return f"Number of new developers committing code within 2 months after the program: {len(new_developers)}\nNew developers: {new_developers_str}"
158
+
159
+ def compare_user_developers_to_others(user_specified_active, other_developers_active, df, program_end_date_str):
160
+ if program_end_date_str is None:
161
+ return "Program end date not provided. Unable to compare user-specified developers to others."
162
+
163
+ program_end_date = pd.to_datetime(program_end_date_str)
164
+
165
+ user_commits = df[(df["developer"].isin(user_specified_active["developer"])) & (df["month_year"] >= program_end_date)]["total_commits"]
166
+ other_commits = df[(df["developer"].isin(other_developers_active["developer"])) & (df["month_year"] >= program_end_date)]["total_commits"]
167
+
168
+ stat, p_value = mannwhitneyu(user_commits, other_commits)
169
+ comparison_result = f"Mann-Whitney U test statistic: {stat:.3f}, P-value: {p_value:.3f}\n"
170
+
171
+ if p_value < 0.25:
172
+ if stat > 0:
173
+ comparison_result += "The user-specified developers have a significantly higher number of commits compared to other developers since the program end date."
174
+ else:
175
+ comparison_result += "The user-specified developers have a significantly lower number of commits compared to other developers since the program end date."
176
+ else:
177
+ comparison_result += "There is no significant difference in the number of commits between user-specified developers and other developers since the program end date."
178
+
179
+ return comparison_result
180
+
181
+ def compare_growth_rate(user_specified_active, other_developers_active, df):
182
+ user_growth_rates = []
183
+ other_growth_rates = []
184
+
185
+ for developer in user_specified_active["developer"].unique():
186
+ user_df = df[df["developer"] == developer]
187
+ user_df = user_df.sort_values("month_year")
188
+ user_commits = user_df["total_commits"].tolist()
189
+ user_growth_rate = calculate_average_growth_rate(user_commits)
190
+ user_growth_rates.append(user_growth_rate)
191
+
192
+ for developer in other_developers_active["developer"].unique():
193
+ other_df = df[df["developer"] == developer]
194
+ other_df = other_df.sort_values("month_year")
195
+ other_commits = other_df["total_commits"].tolist()
196
+ other_growth_rate = calculate_average_growth_rate(other_commits)
197
+ other_growth_rates.append(other_growth_rate)
198
+
199
+ stat, p_value = mannwhitneyu(user_growth_rates, other_growth_rates)
200
+ comparison_result = f"Mann-Whitney U test statistic: {stat:.3f}, P-value: {p_value:.3f}\n"
201
+
202
+ if p_value < 0.25:
203
+ if stat > 0:
204
+ comparison_result += "The user-specified developers have a significantly higher average growth rate of commit activity compared to other developers."
205
+ else:
206
+ comparison_result += "The user-specified developers have a significantly lower average growth rate of commit activity compared to other developers."
207
+ else:
208
+ comparison_result += "There is no significant difference in the average growth rate of commit activity between user-specified developers and other developers."
209
+
210
+ return comparison_result
211
+
212
+ def calculate_average_growth_rate(commits):
213
+ growth_rates = []
214
+ for i in range(1, len(commits)):
215
+ if commits[i - 1] != 0:
216
+ growth_rate = (commits[i] - commits[i - 1]) / commits[i - 1]
217
+ growth_rates.append(growth_rate)
218
+ if len(growth_rates) > 0:
219
+ return sum(growth_rates) / len(growth_rates)
220
+ else:
221
+ return 0
222
+
223
+ def generate_tldr_summary(github_handles, classification_df, analysis_result, new_developers_count, comparison_result, growth_rate_result, event_name):
224
+ summary = f"### 📝 TLDR Summary for {', '.join(github_handles)}\n\n"
225
+
226
+ highly_involved_devs = classification_df[classification_df["Classification"] == "Highly involved"]["Developer"].tolist()
227
+ if highly_involved_devs:
228
+ summary += f"**🌟 High Performers:** {', '.join(highly_involved_devs)}\n\n"
229
+
230
+ if "higher after the program" in analysis_result:
231
+ summary += "**📈 Commit Activity:** Significantly higher after the program.\n\n"
232
+ elif "lower after the program" in analysis_result:
233
+ summary += "**📉 Commit Activity:** Significantly lower after the program.\n\n"
234
+ else:
235
+ summary += "**🔄 Commit Activity:** No significant change after the program.\n\n"
236
+
237
+ if new_developers_count.startswith("Number of new developers"):
238
+ summary += f"**🆕 New Developers:** {new_developers_count.split(':')[1].strip()}\n\n"
239
+
240
+ if "significantly higher number of commits" in comparison_result:
241
+ summary += "**🔍 Comparison with Other Developers:** User-specified developers have a significantly higher number of commits.\n\n"
242
+ elif "significantly lower number of commits" in comparison_result:
243
+ summary += "**🔍 Comparison with Other Developers:** User-specified developers have a significantly lower number of commits.\n\n"
244
+ else:
245
+ summary += "**🔍 Comparison with Other Developers:** No significant difference in the number of commits.\n\n"
246
+
247
+ if "significantly higher average growth rate" in growth_rate_result:
248
+ summary += "**📈 Growth Rate:** User-specified developers have a significantly higher average growth rate.\n\n"
249
+ elif "significantly lower average growth rate" in growth_rate_result:
250
+ summary += "**📉 Growth Rate:** User-specified developers have a significantly lower average growth rate.\n\n"
251
+ else:
252
+ summary += "**🔄 Growth Rate:** No significant difference in the average growth rate.\n\n"
253
+
254
+ if event_name:
255
+ summary += f"*Note: The analysis is based on the {event_name} event.*\n\n"
256
+
257
+ return summary
258
+
259
 
 
260
  with gr.Blocks() as app:
261
+ gr.Markdown("# 🚀 GitHub Starknet Developer Insights")
262
+ gr.Markdown(
263
+ """
264
+ This tool allows you to analyze the GitHub activity of developers within the Starknet ecosystem.
265
+ Enter GitHub handles separated by commas or upload a CSV file with GitHub handles in a single column
266
+ to see their monthly commit activity, involvement classification, and comparisons with other developers.
267
+ """
268
+ )
269
+ with gr.Row():
270
+ with gr.Column():
271
+ text_input = gr.Textbox(
272
+ label="Enter GitHub handles separated by commas",
273
+ placeholder="e.g., user1,user2,user3",
274
+ )
275
+ file_input = gr.File(
276
+ label="Or upload a CSV file with GitHub handles in a single column",
277
+ type="binary",
278
+ )
279
+ gr.Markdown(
280
+ """
281
+ *Note:* When uploading a CSV, ensure it contains a single column of GitHub handles without a header row.
282
+ """
283
+ )
284
+ with gr.Row():
285
+ program_end_date_input = gr.Textbox(label="Program End Date (YYYY-MM-DD)", placeholder="e.g., 2023-06-30")
286
+ event_name_input = gr.Textbox(label="Event Name (optional)", placeholder="e.g., Basecamp, Hackathon")
287
+ gr.Markdown(
288
+ """
289
+ 💡 *Tip: Specifying a program end date allows you to analyze the impact of events like Basecamp or Hackathons on developer activity. Leave it blank to analyze overall activity.*
290
+ """
291
+ )
292
+ btn = gr.Button("Analyze")
293
+
294
+ with gr.Column():
295
+ tldr_output = gr.Markdown(label="📝 TLDR Summary")
296
+
297
  with gr.Row():
298
+ with gr.Column():
299
+ plot_output = gr.Plot(label="📈 Commits per Month")
300
+ with gr.Column():
301
+ box_plot_output = gr.Plot(label="📊 Box Plot Comparison (Last 3 Months)")
302
+
303
+ with gr.Accordion("📊 Statistical Analysis", open=False):
304
+ stat_analysis_output = gr.Textbox(label="Statistical Analysis Results")
305
+ gr.Markdown(
306
+ """
307
+ The Mann-Whitney U test is used to compare the commit activity of developers before and after the program.
308
+ - The test statistic measures the difference in the distribution of commits between the two groups (before and after).
309
+ - The p-value indicates the probability of observing such a difference by chance, assuming there is no real difference between the groups.
310
+ - A p-value less than 0.2 suggests that the difference is considered significant.
311
+ - A positive test statistic indicates that the commit activity is higher after the program, while a negative value indicates lower activity.
312
+ """
313
+ )
314
+
315
+ with gr.Accordion("🆕 New Developers", open=False):
316
+ new_developers_output = gr.Textbox(label="Number of New Developers")
317
+
318
+ with gr.Accordion("🏆 Developer Classification", open=False):
319
+ table_output = gr.Dataframe(label="Developer Classification")
320
+ gr.Markdown(
321
+ """
322
+ ### Developer Classification Criteria
323
+ - **Always been inactive**: No commits have been recorded in the dataset.
324
+ - **Previously active but no longer**: Had commits earlier but none in the last 3 months.
325
+ - **Low-level active**: Fewer than 20 commits in the last 3 months.
326
+ - **Highly involved**: 20 or more commits in the last 3 months.
327
+ """
328
+ )
329
 
330
+ with gr.Accordion("🔍 Comparison with Other Developers", open=False):
331
+ comparison_output = gr.Textbox(label="Comparison with Other Developers")
332
+ gr.Markdown(
333
+ """
334
+ The Mann-Whitney U test is used to compare the commit activity of the user-specified developers with the rest of the developers in the database since the program end date.
335
+ - The test statistic measures the difference in the distribution of commits between the two groups.
336
+ - The p-value indicates the probability of observing such a difference by chance, assuming there is no real difference between the groups.
337
+ - A p-value less than 0.25 suggests that the difference is considered significant.
338
+ - If the test statistic is positive, it means the user-specified developers have a higher number of commits compared to other developers, and vice versa.
339
+ """
340
+ )
341
 
342
+ with gr.Accordion("📈 Growth Rate Comparison", open=False):
343
+ growth_rate_output = gr.Textbox(label="Growth Rate Comparison")
344
+ gr.Markdown(
345
+ """
346
+ The average growth rate of commit activity is compared between the user-specified developers and other developers.
347
+ - The growth rate is calculated as the relative change in the number of commits from one month to the next.
348
+ - The Mann-Whitney U test is used to compare the average growth rates between the two groups.
349
+ - A p-value less than 0.25 suggests that the difference in average growth rates is statistically significant.
350
+ - If the test statistic is positive, it means the user-specified developers have a higher average growth rate compared to other developers, and vice versa.
351
+ """
352
+ )
353
+
354
+ gr.Markdown(
355
+ """
356
+ 💡 *Disclaimer: This information is only for open-source repos and should be taken with a grain of salt. Commits in certain repos may be more important than others, and there are many private repos from several teams that are not included in this analysis.*
357
+ """
358
+ )
359
+
360
+ btn.click(
361
+ process_input,
362
+ inputs=[text_input, file_input, program_end_date_input, event_name_input],
363
+ outputs=[plot_output, box_plot_output, table_output, stat_analysis_output, new_developers_output, comparison_output, growth_rate_output, tldr_output],
364
+ )
365
+
366
+ print(colored("Gradio app initialized.", "blue"))
367
 
368
  if __name__ == "__main__":
369
  print(colored("Launching app...", "blue"))
370
+ app.launch(share=True)
 
github_metrics/utils.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from termcolor import colored
3
+ import pandas as pd
4
+
5
+
6
+ def load_all_developers_dataset():
7
+ try:
8
+ print(colored("Loading dataset...", "blue"))
9
+ df = pd.read_csv("data/source/all_networks_developer_classification.csv")
10
+ df["month_year"] = pd.to_datetime(df["month_year"], format="%B_%Y")
11
+ return df
12
+ except Exception as e:
13
+ print(colored(f"Error loading dataset: {e}", "red"))
14
+ raise
15
+
16
+
17
+ def save_plot(plt, base_filename):
18
+ """
19
+ Save a matplotlib plot to a file with a timestamped filename.
20
+ """
21
+ current_date = datetime.now().strftime("%Y-%m-%d")
22
+ filename = f"data/figures/{base_filename}_{current_date}.png"
23
+ plt.savefig(filename, dpi=300, bbox_inches="tight")
24
+ plt.close()
poetry.lock CHANGED
@@ -84,6 +84,35 @@ tests = ["attrs[tests-no-zope]", "zope-interface"]
84
  tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
85
  tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  [[package]]
88
  name = "certifi"
89
  version = "2024.2.2"
@@ -407,6 +436,29 @@ ufo = ["fs (>=2.2.0,<3)"]
407
  unicode = ["unicodedata2 (>=15.1.0)"]
408
  woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"]
409
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  [[package]]
411
  name = "fsspec"
412
  version = "2024.2.0"
@@ -442,6 +494,17 @@ smb = ["smbprotocol"]
442
  ssh = ["paramiko"]
443
  tqdm = ["tqdm"]
444
 
 
 
 
 
 
 
 
 
 
 
 
445
  [[package]]
446
  name = "gradio"
447
  version = "4.19.2"
@@ -484,6 +547,23 @@ uvicorn = ">=0.14.0"
484
  [package.extras]
485
  oauth = ["authlib", "itsdangerous"]
486
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  [[package]]
488
  name = "gradio-client"
489
  version = "0.10.1"
@@ -617,6 +697,17 @@ files = [
617
  docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
618
  testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"]
619
 
 
 
 
 
 
 
 
 
 
 
 
620
  [[package]]
621
  name = "jinja2"
622
  version = "3.1.3"
@@ -782,6 +873,26 @@ files = [
782
  {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"},
783
  ]
784
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
785
  [[package]]
786
  name = "markdown-it-py"
787
  version = "3.0.0"
@@ -1206,6 +1317,21 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
1206
  typing = ["typing-extensions"]
1207
  xmp = ["defusedxml"]
1208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1209
  [[package]]
1210
  name = "pydantic"
1211
  version = "2.6.2"
@@ -1642,6 +1768,69 @@ files = [
1642
  {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"},
1643
  ]
1644
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1645
  [[package]]
1646
  name = "semantic-version"
1647
  version = "2.10.0"
@@ -1707,6 +1896,20 @@ anyio = ">=3.4.0,<5"
1707
  [package.extras]
1708
  full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
1709
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1710
  [[package]]
1711
  name = "termcolor"
1712
  version = "2.4.0"
@@ -1923,7 +2126,86 @@ files = [
1923
  {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"},
1924
  ]
1925
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1926
  [metadata]
1927
  lock-version = "2.0"
1928
  python-versions = "^3.11"
1929
- content-hash = "e649010056241eabfabb2182a733f26410fa16347d695963195d6438fbf4f95d"
 
84
  tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
85
  tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
86
 
87
+ [[package]]
88
+ name = "autograd"
89
+ version = "1.6.2"
90
+ description = "Efficiently computes derivatives of numpy code."
91
+ optional = false
92
+ python-versions = "*"
93
+ files = [
94
+ {file = "autograd-1.6.2-py3-none-any.whl", hash = "sha256:208dde2a938e63b4f8f5049b1985505139e529068b0d26f8cd7771fd3eb145d5"},
95
+ {file = "autograd-1.6.2.tar.gz", hash = "sha256:8731e08a0c4e389d8695a40072ada4512641c113b6cace8f4cfbe8eb7e9aedeb"},
96
+ ]
97
+
98
+ [package.dependencies]
99
+ future = ">=0.15.2"
100
+ numpy = ">=1.12"
101
+
102
+ [[package]]
103
+ name = "autograd-gamma"
104
+ version = "0.5.0"
105
+ description = "Autograd compatible approximations to the gamma family of functions"
106
+ optional = false
107
+ python-versions = "*"
108
+ files = [
109
+ {file = "autograd-gamma-0.5.0.tar.gz", hash = "sha256:f27abb7b8bb9cffc8badcbf59f3fe44a9db39e124ecacf1992b6d952934ac9c4"},
110
+ ]
111
+
112
+ [package.dependencies]
113
+ autograd = ">=1.2.0"
114
+ scipy = ">=1.2.0"
115
+
116
  [[package]]
117
  name = "certifi"
118
  version = "2024.2.2"
 
436
  unicode = ["unicodedata2 (>=15.1.0)"]
437
  woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"]
438
 
439
+ [[package]]
440
+ name = "formulaic"
441
+ version = "1.0.1"
442
+ description = "An implementation of Wilkinson formulas."
443
+ optional = false
444
+ python-versions = ">=3.7.2"
445
+ files = [
446
+ {file = "formulaic-1.0.1-py3-none-any.whl", hash = "sha256:be9e6127b98ba0293654be528fd070ede264f64d0247477c4d5af799b0b12cf2"},
447
+ {file = "formulaic-1.0.1.tar.gz", hash = "sha256:64dd7992a7aa5bbceb1e40679d0f01fc6f0ba12b7d23d78094a88c2edc68fba1"},
448
+ ]
449
+
450
+ [package.dependencies]
451
+ interface-meta = ">=1.2.0"
452
+ numpy = ">=1.16.5"
453
+ pandas = ">=1.0"
454
+ scipy = ">=1.6"
455
+ typing-extensions = ">=4.2.0"
456
+ wrapt = ">=1.0"
457
+
458
+ [package.extras]
459
+ arrow = ["pyarrow (>=1)"]
460
+ calculus = ["sympy (>=1.3,!=1.10)"]
461
+
462
  [[package]]
463
  name = "fsspec"
464
  version = "2024.2.0"
 
494
  ssh = ["paramiko"]
495
  tqdm = ["tqdm"]
496
 
497
+ [[package]]
498
+ name = "future"
499
+ version = "1.0.0"
500
+ description = "Clean single-source support for Python 3 and 2"
501
+ optional = false
502
+ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
503
+ files = [
504
+ {file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"},
505
+ {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"},
506
+ ]
507
+
508
  [[package]]
509
  name = "gradio"
510
  version = "4.19.2"
 
547
  [package.extras]
548
  oauth = ["authlib", "itsdangerous"]
549
 
550
+ [[package]]
551
+ name = "gradio-calendar"
552
+ version = "0.0.4"
553
+ description = "Gradio component for selecting dates with a calendar 📆"
554
+ optional = false
555
+ python-versions = ">=3.8"
556
+ files = [
557
+ {file = "gradio_calendar-0.0.4-py3-none-any.whl", hash = "sha256:32f738ea94ed4dea1e4069a117d907fc3e8a2757fb2f4b0ce67a801f434928df"},
558
+ {file = "gradio_calendar-0.0.4.tar.gz", hash = "sha256:e4ea93cc7f6284bb8e547bb34d891776f1f0bebd320eae80bd644c0cd30694ce"},
559
+ ]
560
+
561
+ [package.dependencies]
562
+ gradio = ">=4.0,<5.0"
563
+
564
+ [package.extras]
565
+ dev = ["build", "twine"]
566
+
567
  [[package]]
568
  name = "gradio-client"
569
  version = "0.10.1"
 
697
  docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
698
  testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"]
699
 
700
+ [[package]]
701
+ name = "interface-meta"
702
+ version = "1.3.0"
703
+ description = "`interface_meta` provides a convenient way to expose an extensible API with enforced method signatures and consistent documentation."
704
+ optional = false
705
+ python-versions = ">=3.7,<4.0"
706
+ files = [
707
+ {file = "interface_meta-1.3.0-py3-none-any.whl", hash = "sha256:de35dc5241431886e709e20a14d6597ed07c9f1e8b4bfcffde2190ca5b700ee8"},
708
+ {file = "interface_meta-1.3.0.tar.gz", hash = "sha256:8a4493f8bdb73fb9655dcd5115bc897e207319e36c8835f39c516a2d7e9d79a1"},
709
+ ]
710
+
711
  [[package]]
712
  name = "jinja2"
713
  version = "3.1.3"
 
873
  {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"},
874
  ]
875
 
876
+ [[package]]
877
+ name = "lifelines"
878
+ version = "0.28.0"
879
+ description = "Survival analysis in Python, including Kaplan Meier, Nelson Aalen and regression"
880
+ optional = false
881
+ python-versions = ">=3.9"
882
+ files = [
883
+ {file = "lifelines-0.28.0-py3-none-any.whl", hash = "sha256:eacade0527a62d6086cb76f7dbf1e94f2f445a696dd7168723bc7a86e4737a99"},
884
+ {file = "lifelines-0.28.0.tar.gz", hash = "sha256:eecf726453fd409c94fef8a521f8e593bcd09337f920fe885131f01cfe58b25e"},
885
+ ]
886
+
887
+ [package.dependencies]
888
+ autograd = ">=1.5"
889
+ autograd-gamma = ">=0.3"
890
+ formulaic = ">=0.2.2"
891
+ matplotlib = ">=3.0"
892
+ numpy = ">=1.14.0,<2.0"
893
+ pandas = ">=1.2.0"
894
+ scipy = ">=1.2.0"
895
+
896
  [[package]]
897
  name = "markdown-it-py"
898
  version = "3.0.0"
 
1317
  typing = ["typing-extensions"]
1318
  xmp = ["defusedxml"]
1319
 
1320
+ [[package]]
1321
+ name = "plotly"
1322
+ version = "5.19.0"
1323
+ description = "An open-source, interactive data visualization library for Python"
1324
+ optional = false
1325
+ python-versions = ">=3.8"
1326
+ files = [
1327
+ {file = "plotly-5.19.0-py3-none-any.whl", hash = "sha256:906abcc5f15945765328c5d47edaa884bc99f5985fbc61e8cd4dc361f4ff8f5a"},
1328
+ {file = "plotly-5.19.0.tar.gz", hash = "sha256:5ea91a56571292ade3e3bc9bf712eba0b95a1fb0a941375d978cc79432e055f4"},
1329
+ ]
1330
+
1331
+ [package.dependencies]
1332
+ packaging = "*"
1333
+ tenacity = ">=6.2.0"
1334
+
1335
  [[package]]
1336
  name = "pydantic"
1337
  version = "2.6.2"
 
1768
  {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"},
1769
  ]
1770
 
1771
+ [[package]]
1772
+ name = "scipy"
1773
+ version = "1.12.0"
1774
+ description = "Fundamental algorithms for scientific computing in Python"
1775
+ optional = false
1776
+ python-versions = ">=3.9"
1777
+ files = [
1778
+ {file = "scipy-1.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:78e4402e140879387187f7f25d91cc592b3501a2e51dfb320f48dfb73565f10b"},
1779
+ {file = "scipy-1.12.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:f5f00ebaf8de24d14b8449981a2842d404152774c1a1d880c901bf454cb8e2a1"},
1780
+ {file = "scipy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e53958531a7c695ff66c2e7bb7b79560ffdc562e2051644c5576c39ff8efb563"},
1781
+ {file = "scipy-1.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e32847e08da8d895ce09d108a494d9eb78974cf6de23063f93306a3e419960c"},
1782
+ {file = "scipy-1.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4c1020cad92772bf44b8e4cdabc1df5d87376cb219742549ef69fc9fd86282dd"},
1783
+ {file = "scipy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:75ea2a144096b5e39402e2ff53a36fecfd3b960d786b7efd3c180e29c39e53f2"},
1784
+ {file = "scipy-1.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:408c68423f9de16cb9e602528be4ce0d6312b05001f3de61fe9ec8b1263cad08"},
1785
+ {file = "scipy-1.12.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5adfad5dbf0163397beb4aca679187d24aec085343755fcdbdeb32b3679f254c"},
1786
+ {file = "scipy-1.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3003652496f6e7c387b1cf63f4bb720951cfa18907e998ea551e6de51a04467"},
1787
+ {file = "scipy-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b8066bce124ee5531d12a74b617d9ac0ea59245246410e19bca549656d9a40a"},
1788
+ {file = "scipy-1.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8bee4993817e204d761dba10dbab0774ba5a8612e57e81319ea04d84945375ba"},
1789
+ {file = "scipy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a24024d45ce9a675c1fb8494e8e5244efea1c7a09c60beb1eeb80373d0fecc70"},
1790
+ {file = "scipy-1.12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e7e76cc48638228212c747ada851ef355c2bb5e7f939e10952bc504c11f4e372"},
1791
+ {file = "scipy-1.12.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f7ce148dffcd64ade37b2df9315541f9adad6efcaa86866ee7dd5db0c8f041c3"},
1792
+ {file = "scipy-1.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c39f92041f490422924dfdb782527a4abddf4707616e07b021de33467f917bc"},
1793
+ {file = "scipy-1.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7ebda398f86e56178c2fa94cad15bf457a218a54a35c2a7b4490b9f9cb2676c"},
1794
+ {file = "scipy-1.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:95e5c750d55cf518c398a8240571b0e0782c2d5a703250872f36eaf737751338"},
1795
+ {file = "scipy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e646d8571804a304e1da01040d21577685ce8e2db08ac58e543eaca063453e1c"},
1796
+ {file = "scipy-1.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:913d6e7956c3a671de3b05ccb66b11bc293f56bfdef040583a7221d9e22a2e35"},
1797
+ {file = "scipy-1.12.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba1b0c7256ad75401c73e4b3cf09d1f176e9bd4248f0d3112170fb2ec4db067"},
1798
+ {file = "scipy-1.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:730badef9b827b368f351eacae2e82da414e13cf8bd5051b4bdfd720271a5371"},
1799
+ {file = "scipy-1.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6546dc2c11a9df6926afcbdd8a3edec28566e4e785b915e849348c6dd9f3f490"},
1800
+ {file = "scipy-1.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:196ebad3a4882081f62a5bf4aeb7326aa34b110e533aab23e4374fcccb0890dc"},
1801
+ {file = "scipy-1.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:b360f1b6b2f742781299514e99ff560d1fe9bd1bff2712894b52abe528d1fd1e"},
1802
+ {file = "scipy-1.12.0.tar.gz", hash = "sha256:4bf5abab8a36d20193c698b0f1fc282c1d083c94723902c447e5d2f1780936a3"},
1803
+ ]
1804
+
1805
+ [package.dependencies]
1806
+ numpy = ">=1.22.4,<1.29.0"
1807
+
1808
+ [package.extras]
1809
+ dev = ["click", "cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"]
1810
+ doc = ["jupytext", "matplotlib (>2)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"]
1811
+ test = ["asv", "gmpy2", "hypothesis", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"]
1812
+
1813
+ [[package]]
1814
+ name = "seaborn"
1815
+ version = "0.13.2"
1816
+ description = "Statistical data visualization"
1817
+ optional = false
1818
+ python-versions = ">=3.8"
1819
+ files = [
1820
+ {file = "seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987"},
1821
+ {file = "seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7"},
1822
+ ]
1823
+
1824
+ [package.dependencies]
1825
+ matplotlib = ">=3.4,<3.6.1 || >3.6.1"
1826
+ numpy = ">=1.20,<1.24.0 || >1.24.0"
1827
+ pandas = ">=1.2"
1828
+
1829
+ [package.extras]
1830
+ dev = ["flake8", "flit", "mypy", "pandas-stubs", "pre-commit", "pytest", "pytest-cov", "pytest-xdist"]
1831
+ docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx (<6.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-issues"]
1832
+ stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"]
1833
+
1834
  [[package]]
1835
  name = "semantic-version"
1836
  version = "2.10.0"
 
1896
  [package.extras]
1897
  full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
1898
 
1899
+ [[package]]
1900
+ name = "tenacity"
1901
+ version = "8.2.3"
1902
+ description = "Retry code until it succeeds"
1903
+ optional = false
1904
+ python-versions = ">=3.7"
1905
+ files = [
1906
+ {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"},
1907
+ {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"},
1908
+ ]
1909
+
1910
+ [package.extras]
1911
+ doc = ["reno", "sphinx", "tornado (>=4.5)"]
1912
+
1913
  [[package]]
1914
  name = "termcolor"
1915
  version = "2.4.0"
 
2126
  {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"},
2127
  ]
2128
 
2129
+ [[package]]
2130
+ name = "wrapt"
2131
+ version = "1.16.0"
2132
+ description = "Module for decorators, wrappers and monkey patching."
2133
+ optional = false
2134
+ python-versions = ">=3.6"
2135
+ files = [
2136
+ {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"},
2137
+ {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"},
2138
+ {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"},
2139
+ {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"},
2140
+ {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"},
2141
+ {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"},
2142
+ {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"},
2143
+ {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"},
2144
+ {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"},
2145
+ {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"},
2146
+ {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"},
2147
+ {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"},
2148
+ {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"},
2149
+ {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"},
2150
+ {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"},
2151
+ {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"},
2152
+ {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"},
2153
+ {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"},
2154
+ {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"},
2155
+ {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"},
2156
+ {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"},
2157
+ {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"},
2158
+ {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"},
2159
+ {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"},
2160
+ {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"},
2161
+ {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"},
2162
+ {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"},
2163
+ {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"},
2164
+ {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"},
2165
+ {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"},
2166
+ {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"},
2167
+ {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"},
2168
+ {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"},
2169
+ {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"},
2170
+ {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"},
2171
+ {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"},
2172
+ {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"},
2173
+ {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"},
2174
+ {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"},
2175
+ {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"},
2176
+ {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"},
2177
+ {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"},
2178
+ {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"},
2179
+ {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"},
2180
+ {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"},
2181
+ {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"},
2182
+ {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"},
2183
+ {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"},
2184
+ {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"},
2185
+ {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"},
2186
+ {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"},
2187
+ {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"},
2188
+ {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"},
2189
+ {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"},
2190
+ {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"},
2191
+ {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"},
2192
+ {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"},
2193
+ {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"},
2194
+ {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"},
2195
+ {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"},
2196
+ {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"},
2197
+ {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"},
2198
+ {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"},
2199
+ {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"},
2200
+ {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"},
2201
+ {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"},
2202
+ {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"},
2203
+ {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"},
2204
+ {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"},
2205
+ {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"},
2206
+ ]
2207
+
2208
  [metadata]
2209
  lock-version = "2.0"
2210
  python-versions = "^3.11"
2211
+ content-hash = "93c07a0e554c6697440ab4476e4eaa56b3e41e1d857b3052df1d13e27ba77c4c"
pyproject.toml CHANGED
@@ -12,6 +12,12 @@ gradio = "^4.19.2"
12
  pandas = "^2.2.1"
13
  matplotlib = "^3.8.3"
14
  termcolor = "^2.4.0"
 
 
 
 
 
 
15
 
16
 
17
  [build-system]
 
12
  pandas = "^2.2.1"
13
  matplotlib = "^3.8.3"
14
  termcolor = "^2.4.0"
15
+ seaborn = "^0.13.2"
16
+ numpy = "^1.26.4"
17
+ lifelines = "^0.28.0"
18
+ plotly = "^5.19.0"
19
+ gradio-calendar = "^0.0.4"
20
+ scipy = "^1.12.0"
21
 
22
 
23
  [build-system]