Skip to content

API Reference

I/O Functions

load_and_merge_csv(filepaths)

Load and concatenate multiple FAOSTAT CSV files into a single DataFrame.

Parameters

filepaths : list of str List of paths to the CSV files.

Returns

pd.DataFrame A single DataFrame resulting from concatenation of all input files.

Source code in faonet/io.py
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def load_and_merge_csv(filepaths):
    """
    Load and concatenate multiple FAOSTAT CSV files into a single DataFrame.

    Parameters
    ----------
    filepaths : list of str
        List of paths to the CSV files.

    Returns
    -------
    pd.DataFrame
        A single DataFrame resulting from concatenation of all input files.
    """

    dataframes = [pd.read_csv(path) for path in filepaths]
    return pd.concat(dataframes, ignore_index=True)

load_file(file, year=2023)

Load a single FAOSTAT CSV file and filter by a specific year.

Parameters

file : str or path-like Path to the CSV file. year : int, optional Year to filter the data by (default is 2023).

Returns

pd.DataFrame DataFrame filtered to only include data from the specified year.

Source code in faonet/io.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def load_file(file, year=2023):
    """
    Load a single FAOSTAT CSV file and filter by a specific year.

    Parameters
    ----------
    file : str or path-like
        Path to the CSV file.
    year : int, optional
        Year to filter the data by (default is 2023).

    Returns
    -------
    pd.DataFrame
        DataFrame filtered to only include data from the specified year.
    """

    dataframes = pd.read_csv(file)
    return dataframes[dataframes['Year'] == year]

save_dataframe(df, filepath)

Save a pandas DataFrame to a CSV file.

Parameters

df : pd.DataFrame The DataFrame to save. filepath : str Destination path for the output CSV file.

Source code in faonet/io.py
42
43
44
45
46
47
48
49
50
51
52
53
def save_dataframe(df, filepath):
    """
    Save a pandas DataFrame to a CSV file.

    Parameters
    ----------
    df : pd.DataFrame
        The DataFrame to save.
    filepath : str
        Destination path for the output CSV file.
    """
    df.to_csv(filepath, index=False)

Metrics

compute_betweenness_all(G)

Compute multiple betweenness centrality measures for a bipartite network.

This function calculates: - Betweenness in the full bipartite network using both real and inverted weights. - Betweenness in the projected graphs (for exporters and importers), again with real and inverted weights.

Parameters

G : networkx.Graph Bipartite graph with edge attribute 'weight'.

Returns

pd.DataFrame DataFrame with one row per node and the following columns: - 'node': Node identifier - 'bipartite_set': 0 if exporter, 1 if importer - 'betweenness_bipartite': Centrality in full bipartite graph (weights) - 'betweenness_bipartite_inv': Centrality in full bipartite graph (inverted weights) - 'betweenness_proj_exporters': Centrality in exporter projection (weights) - 'betweenness_proj_exporters_inv': Centrality in exporter projection (inverted weights) - 'betweenness_proj_importers': Centrality in importer projection (weights) - 'betweenness_proj_importers_inv': Centrality in importer projection (inverted weights)

Source code in faonet/metrics.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def compute_betweenness_all(G):
    """
    Compute multiple betweenness centrality measures for a bipartite network.

    This function calculates:
    - Betweenness in the full bipartite network using both real and inverted weights.
    - Betweenness in the projected graphs (for exporters and importers), again with real and inverted weights.

    Parameters
    ----------
    G : networkx.Graph
        Bipartite graph with edge attribute 'weight'.

    Returns
    -------
    pd.DataFrame
        DataFrame with one row per node and the following columns:
        - 'node': Node identifier
        - 'bipartite_set': 0 if exporter, 1 if importer
        - 'betweenness_bipartite': Centrality in full bipartite graph (weights)
        - 'betweenness_bipartite_inv': Centrality in full bipartite graph (inverted weights)
        - 'betweenness_proj_exporters': Centrality in exporter projection (weights)
        - 'betweenness_proj_exporters_inv': Centrality in exporter projection (inverted weights)
        - 'betweenness_proj_importers': Centrality in importer projection (weights)
        - 'betweenness_proj_importers_inv': Centrality in importer projection (inverted weights)
    """
    # Identify bipartite sets
    exportadores = {n for n, d in G.nodes(data=True) if d.get("bipartite") == 0}
    importadores = set(G) - exportadores

    # Invert weights for shortest-path based betweenness
    G_inv = G.copy()
    for u, v, d in G_inv.edges(data=True):
        peso = d.get("weight", 1)
        d["inv_weight"] = 1 / peso if peso > 0 else 0

    # Betweenness in original bipartite network
    bet_bip = nx.betweenness_centrality(G, weight="weight")
    bet_bip_inv = nx.betweenness_centrality(G_inv, weight="inv_weight")

    # Projected graphs
    proy_exp = bipartite.weighted_projected_graph(G, exportadores)
    proy_imp = bipartite.weighted_projected_graph(G, importadores)

    # Betweenness in projections (real weights)
    bet_proy_exp = nx.betweenness_centrality(proy_exp, weight="weight")
    bet_proy_imp = nx.betweenness_centrality(proy_imp, weight="weight")

    # Invert weights in projections
    for _, _, d in proy_exp.edges(data=True):
        d["inv_weight"] = 1 / d["weight"] if d["weight"] > 0 else 0
    for _, _, d in proy_imp.edges(data=True):
        d["inv_weight"] = 1 / d["weight"] if d["weight"] > 0 else 0

    bet_proy_exp_inv = nx.betweenness_centrality(proy_exp, weight="inv_weight")
    bet_proy_imp_inv = nx.betweenness_centrality(proy_imp, weight="inv_weight")

    # Build results
    nodos = list(G.nodes())
    df_bet = pd.DataFrame({
        "node": nodos,
        "bipartite_set": [G.nodes[n].get("bipartite") for n in nodos],
        "betweenness_bipartite": [bet_bip.get(n, 0) for n in nodos],
        "betweenness_bipartite_inv": [bet_bip_inv.get(n, 0) for n in nodos],
        "betweenness_proj_exporters": [bet_proy_exp.get(n, None) for n in nodos],
        "betweenness_proj_exporters_inv": [bet_proy_exp_inv.get(n, None) for n in nodos],
        "betweenness_proj_importers": [bet_proy_imp.get(n, None) for n in nodos],
        "betweenness_proj_importers_inv": [bet_proy_imp_inv.get(n, None) for n in nodos],
    })

    return df_bet

compute_bipartite_clustering(G, reporters=None, normalized=True)

Compute bipartite clustering coefficients C4b and C4b^w for each node in a bipartite graph.

Parameters

G : networkx.Graph Bipartite graph with edge attribute 'weight'. reporters (set, optional): Set of nodes considered "Exportadores". All others will be labeled "Importadores" if this is provided. normalized (bool): Whether to use normalized version of the clustering.

Returns

pd.DataFrame: DataFrame with C4b, C4b^w, their ratio, degree and type.

Source code in faonet/metrics.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def compute_bipartite_clustering(G, reporters=None, normalized=True):
    """
    Compute bipartite clustering coefficients C4b and C4b^w for each node in a bipartite graph.

    Parameters
    ----------
    G : networkx.Graph
        Bipartite graph with edge attribute 'weight'.
        reporters (set, optional): Set of nodes considered "Exportadores". 
                                   All others will be labeled "Importadores" if this is provided.
        normalized (bool): Whether to use normalized version of the clustering.

    Returns
    -------
    pd.DataFrame: 
        DataFrame with C4b, C4b^w, their ratio, degree and type.
    """

    def c4b_node(G, node):
        neighbors = list(G[node])
        k_i = len(neighbors)
        s_i = sum(G[node][n].get("weight", 1) for n in neighbors)

        if k_i < 2:
            return 0.0, 0.0

        neighbor_pairs = list(itertools.combinations(neighbors, 2))
        q_i = 0
        qw_i = 0.0

        for m, n in neighbor_pairs:
            neighbors_m = set(G[m])
            neighbors_n = set(G[n])
            common = neighbors_m & neighbors_n - {node}

            for v in common:
                q_i += 1
                w_im = G[node][m].get("weight", 1)
                w_in = G[node][n].get("weight", 1)
                wnorm_m = w_im / s_i if s_i > 0 else 0
                wnorm_n = w_in / s_i if s_i > 0 else 0
                qw_i += (wnorm_m + wnorm_n) / 2

        # Normalization term
        k_nn = len(set.union(*(set(G[n]) for n in neighbors)) - {node})
        Q_i = k_i * (k_i - 1) / 2 * k_nn if normalized else 1

        C4b = q_i / Q_i if Q_i > 0 else 0
        C4bw = qw_i / Q_i if Q_i > 0 else 0
        return C4b, C4bw

    # Compute clustering for all nodes
    results = []
    for node in G.nodes():
        c4b, c4bw = c4b_node(G, node)
        results.append({
            "node": node,
            "C4b": c4b,
            "C4b^w": c4bw,
            "degree": G.degree(node)
        })

    df = pd.DataFrame(results)
    df["C4_rate"] = df["C4b^w"] / df["C4b"]
    df.replace([np.inf, -np.inf], np.nan, inplace=True)

    if reporters is not None:
        df["tipo"] = df["node"].apply(lambda x: "Exportador" if x in reporters else "Importador")

    return df

compute_degree_and_strength(B, reporters, partners)

Compute the degree and strength (sum of edge weights) for nodes in a bipartite network.

Parameters

B : networkx.Graph Bipartite graph with weights on the edges (under the 'weight' attribute). reporters : set Set of nodes in one bipartite group (e.g., exporters). partners : set Set of nodes in the other bipartite group (e.g., importers).

Returns

tuple of pd.DataFrame (df_exporters, df_importers): - df_exporters : DataFrame with 'Degree' and 'Strength' for reporter nodes. - df_importers : DataFrame with 'Degree' and 'Strength' for partner nodes.

Compute degree and strength (sum of weights) for nodes in a bipartite network.

Source code in faonet/metrics.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def compute_degree_and_strength(B, reporters, partners):
    """
    Compute the degree and strength (sum of edge weights) for nodes in a bipartite network.

    Parameters
    ----------
    B : networkx.Graph
        Bipartite graph with weights on the edges (under the 'weight' attribute).
    reporters : set
        Set of nodes in one bipartite group (e.g., exporters).
    partners : set
        Set of nodes in the other bipartite group (e.g., importers).

    Returns
    -------
    tuple of pd.DataFrame
        (df_exporters, df_importers):
        - df_exporters : DataFrame with 'Degree' and 'Strength' for reporter nodes.
        - df_importers : DataFrame with 'Degree' and 'Strength' for partner nodes.

    Compute degree and strength (sum of weights) for nodes in a bipartite network.
    """
    # Compute strength: sum of edge weights per node
    strength = {
        node: sum(data['weight'] for _, _, data in B.edges(node, data=True))
        for node in B.nodes()
    }

    # Compute degree using built-in function
    degree = dict(B.degree())

    # Separate by node group
    exporters_strength = {node: strength[node] for node in reporters}
    importers_strength = {node: strength[node] for node in partners}
    exporters_degree = {node: degree[node] for node in reporters}
    importers_degree = {node: degree[node] for node in partners}

    # Create dataframes
    df_exporters = pd.DataFrame({
        "Degree": pd.Series(exporters_degree),
        "Strength": pd.Series(exporters_strength)
    }).dropna()

    df_importers = pd.DataFrame({
        "Degree": pd.Series(importers_degree),
        "Strength": pd.Series(importers_strength)
    }).dropna()

    return df_exporters, df_importers

degree_by_group(G, group_nodes)

Compute the degree (number of connections) for a given group of nodes.

Parameters

G : networkx.Graph The network graph. group_nodes : iterable Set or list of nodes for which to compute the degree.

Returns

pd.DataFrame DataFrame with columns ['Node', 'Degree'].

Source code in faonet/metrics.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def degree_by_group(G, group_nodes):
    """
    Compute the degree (number of connections) for a given group of nodes.

    Parameters
    ----------
    G : networkx.Graph
        The network graph.
    group_nodes : iterable
        Set or list of nodes for which to compute the degree.

    Returns
    -------
    pd.DataFrame
        DataFrame with columns ['Node', 'Degree'].
    """
    degrees = {n: G.degree(n) for n in group_nodes}
    return pd.DataFrame(degrees.items(), columns=["Node", "Degree"])

Network Builder

build_bipartite_network(df, reporter_col, partner_col, weight_col)

Construct a bipartite network from a FAOSTAT-style trade DataFrame.

Parameters

df : pd.DataFrame The input data containing trade flows. reporter_col : str Column name for exporter (reporter) countries. partner_col : str Column name for importer (partner) countries. weight_col : str Column name for trade volume or weight of the connection.

Returns

B : networkx.Graph A bipartite NetworkX graph with edge weights. reporters : set Set of nodes representing exporters (bipartite=0). partners : set Set of nodes representing importers (bipartite=1).

Source code in faonet/network.py
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def build_bipartite_network(df, reporter_col, partner_col, weight_col):
    """
    Construct a bipartite network from a FAOSTAT-style trade DataFrame.

    Parameters
    ----------
    df : pd.DataFrame
        The input data containing trade flows.
    reporter_col : str
        Column name for exporter (reporter) countries.
    partner_col : str
        Column name for importer (partner) countries.
    weight_col : str
        Column name for trade volume or weight of the connection.

    Returns
    -------
    B : networkx.Graph
        A bipartite NetworkX graph with edge weights.
    reporters : set
        Set of nodes representing exporters (bipartite=0).
    partners : set
        Set of nodes representing importers (bipartite=1).
    """
    B = nx.Graph()
    reporters = set(df[reporter_col])
    partners = set(df[partner_col])

    B.add_nodes_from(reporters, bipartite=0)
    B.add_nodes_from(partners, bipartite=1)

    for _, row in df.iterrows():
        B.add_edge(row[reporter_col], row[partner_col], weight=row[weight_col])

    return B, reporters, partners

remove_zero_weight_edges(G)

Remove all edges with zero weight from a NetworkX graph.

Parameters

G : networkx.Graph The input graph, which must contain a 'weight' attribute on edges.

Returns

G : networkx.Graph The modified graph with zero-weight edges removed.

Source code in faonet/network.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def remove_zero_weight_edges(G):
    """
    Remove all edges with zero weight from a NetworkX graph.

    Parameters
    ----------
    G : networkx.Graph
        The input graph, which must contain a 'weight' attribute on edges.

    Returns
    -------
    G : networkx.Graph
        The modified graph with zero-weight edges removed.
    """
    zero_edges = [(u, v) for u, v, d in G.edges(data=True) if d.get("weight", 1) == 0]
    G.remove_edges_from(zero_edges)
    return G

Filtering

filter_top_percentile(df, value_column='Value', percentile=0.9)

Filter a DataFrame to retain rows that account for a given cumulative percentile of a value column.

Parameters

df : pd.DataFrame Input DataFrame to be filtered. value_column : str Column name to use for cumulative sum and filtering (e.g., trade value). percentile : float Cumulative threshold to retain (between 0 and 1, e.g., 0.9 for top 90%).

Returns

pd.DataFrame Filtered DataFrame containing only the rows that fall within the specified cumulative percentile.

Source code in faonet/filtering.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def filter_top_percentile(df, value_column="Value", percentile=0.9):
    """
    Filter a DataFrame to retain rows that account for a given cumulative percentile of a value column.

    Parameters
    ----------
    df : pd.DataFrame
        Input DataFrame to be filtered.
    value_column : str
        Column name to use for cumulative sum and filtering (e.g., trade value).
    percentile : float
        Cumulative threshold to retain (between 0 and 1, e.g., 0.9 for top 90%).

    Returns
    -------
    pd.DataFrame
        Filtered DataFrame containing only the rows that fall within the specified cumulative percentile.

    """
    df_sorted = df.sort_values(by=value_column, ascending=False)
    total_value = df_sorted[value_column].sum()
    df_sorted["cumsum"] = df_sorted[value_column].cumsum()
    df_sorted["cumperc"] = df_sorted["cumsum"] / total_value
    return df_sorted[df_sorted["cumperc"] <= percentile].copy()

Plotting

plot_betweenness_heatmap(betweenness_data, metric_col, years=None, label_col='node', mode='rank', top_n=10, nodes=None, ascending=False, cmap='YlOrRd', figsize=(8, 6), title=None, cbar_label=None, annot=True, fmt='.0f', save_path=None, save_dpi=300, save_bbox_inches='tight')

Plot a heatmap showing the evolution of betweenness values or ranks across years.

Parameters

betweenness_data : dict Dictionary mapping year -> DataFrame containing betweenness results. metric_col : str Column name with the betweenness metric to visualize. years : list, optional Ordered list of years to include. If None, uses sorted keys. label_col : str Column name with node labels. mode : {"rank", "value"} Whether to plot within-year rank or raw metric values. Rank=1 is the most central. top_n : int Number of nodes to include when nodes is not provided. nodes : list or None Specific node labels to include. If None, selects the top nodes by average score. ascending : bool Sorting direction for metric values when computing ranks. cmap : str Colormap for the heatmap. figsize : tuple Figure size in inches. title : str, optional Title of the plot. cbar_label : str, optional Label for the color bar. annot : bool Whether to annotate heatmap cells. fmt : str Format string for annotations. save_path : str or None If provided, save the figure to this path. save_dpi : int Resolution used when saving the figure. save_bbox_inches : str Bounding box option passed to savefig.

Returns

tuple (ax, matrix) where matrix is the DataFrame displayed in the heatmap.

Source code in faonet/plots.py
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
def plot_betweenness_heatmap(
    betweenness_data,
    metric_col,
    years=None,
    label_col="node",
    mode="rank",
    top_n=10,
    nodes=None,
    ascending=False,
    cmap="YlOrRd",
    figsize=(8, 6),
    title=None,
    cbar_label=None,
    annot=True,
    fmt=".0f",
    save_path=None,
    save_dpi=300,
    save_bbox_inches="tight",
):
    """
    Plot a heatmap showing the evolution of betweenness values or ranks across years.

    Parameters
    ----------
    betweenness_data : dict
        Dictionary mapping year -> DataFrame containing betweenness results.
    metric_col : str
        Column name with the betweenness metric to visualize.
    years : list, optional
        Ordered list of years to include. If None, uses sorted keys.
    label_col : str
        Column name with node labels.
    mode : {"rank", "value"}
        Whether to plot within-year rank or raw metric values. Rank=1 is the most central. 
    top_n : int
        Number of nodes to include when `nodes` is not provided.
    nodes : list or None
        Specific node labels to include. If None, selects the top nodes by average score.
    ascending : bool
        Sorting direction for metric values when computing ranks.
    cmap : str
        Colormap for the heatmap.
    figsize : tuple
        Figure size in inches.
    title : str, optional
        Title of the plot.
    cbar_label : str, optional
        Label for the color bar.
    annot : bool
        Whether to annotate heatmap cells.
    fmt : str
        Format string for annotations.
    save_path : str or None
        If provided, save the figure to this path.
    save_dpi : int
        Resolution used when saving the figure.
    save_bbox_inches : str
        Bounding box option passed to `savefig`.

    Returns
    -------
    tuple
        (ax, matrix) where `matrix` is the DataFrame displayed in the heatmap.
    """
    if years is None:
        years = sorted(betweenness_data)

    if not years:
        raise ValueError("No years were provided in betweenness_data.")

    frames = []
    for year in years:
        if year not in betweenness_data:
            continue

        df_year = betweenness_data[year][[label_col, metric_col]].copy()
        df_year["year"] = year

        if mode == "rank":
            df_year["display_value"] = df_year[metric_col].rank(
                method="min",
                ascending=ascending
            )
        elif mode == "value":
            df_year["display_value"] = df_year[metric_col]
        else:
            raise ValueError("mode must be either 'rank' or 'value'.")

        frames.append(df_year)

    combined = pd.concat(frames, ignore_index=True)

    if nodes is None:
        selector = (
            combined.groupby(label_col)["display_value"]
            .mean()
            .sort_values(ascending=(mode == "rank"))
        )
        selected_nodes = list(selector.head(top_n).index)
    else:
        selected_nodes = list(nodes)

    matrix = (
        combined[combined[label_col].isin(selected_nodes)]
        .pivot(index=label_col, columns="year", values="display_value")
        .reindex(selected_nodes)
    )

    if mode == "rank":
        fmt = ".0f"
        if cbar_label is None:
            cbar_label = "Rank"
    elif cbar_label is None:
        cbar_label = metric_col

    fig, ax = plt.subplots(figsize=figsize)
    sns.heatmap(
        matrix,
        cmap=cmap,
        annot=annot,
        fmt=fmt,
        linewidths=0.5,
        cbar_kws={"label": cbar_label},
        ax=ax,
    )

    ax.set_xlabel("Year")
    ax.set_ylabel("")
    ax.set_title(title or f"Betweenness {mode.capitalize()} Heatmap")
    fig.tight_layout()

    if save_path is not None:
        fig.savefig(save_path, dpi=save_dpi, bbox_inches=save_bbox_inches)

    return ax, matrix

plot_bipartite_network2(B, group0_nodes, title=None, figsize=(12, 8), node_size=700, font_size=10)

Plot a bipartite network using NetworkX with edge weights shown as color intensity.

Parameters

B : networkx.Graph Bipartite graph with 'weight' attributes on edges. group0_nodes : list or set Nodes from one bipartite group (used for layout positioning). title : str, optional Title of the plot. figsize : tuple Figure size in inches. node_size : int Size of the nodes in the plot. font_size : int Font size for node labels.

Returns

matplotlib.axes.Axes The matplotlib Axes object of the plot.

Source code in faonet/plots.py
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def plot_bipartite_network2(B, group0_nodes, title=None, figsize=(12, 8), node_size=700, font_size=10):
    """
    Plot a bipartite network using NetworkX with edge weights shown as color intensity.

    Parameters
    ----------
    B : networkx.Graph
        Bipartite graph with 'weight' attributes on edges.
    group0_nodes : list or set
        Nodes from one bipartite group (used for layout positioning).
    title : str, optional
        Title of the plot.
    figsize : tuple
        Figure size in inches.
    node_size : int
        Size of the nodes in the plot.
    font_size : int
        Font size for node labels.

    Returns
    -------
    matplotlib.axes.Axes
        The matplotlib Axes object of the plot.
    """
    fig, ax = plt.subplots(figsize=figsize)

    # Layout
    pos = nx.bipartite_layout(B, group0_nodes)

    # Extract weights
    edges = B.edges(data=True)
    weights = [d['weight'] for (_, _, d) in edges]
    max_weight = max(weights) if weights else 1  # avoid division by zero

    # Draw network
    nx.draw(
        B, pos, ax=ax, with_labels=True, node_size=node_size, font_size=font_size,
        edge_color=weights,
        width=[w / max_weight * 5 for w in weights],
        edge_cmap=plt.cm.Blues
    )

    if title:
        ax.set_title(title)

    plt.tight_layout()
    return ax

plot_bipartite_network_enhanced(B, group0_nodes, title=None, figsize=(14, 9), exporter_color='#0072B2', importer_color='#D55E00', edge_cmap='Greys', edge_alpha=0.45, edge_width_scale=4.0, label_top_n=10, font_size=13, title_font_size=18, node_size_mode='strength', node_size_range=(250, 1400), default_node_size=700, partition_gap=1.2, label_offset=0.04, label_min_gap=0.055, draw_label_connectors=True, x_margin_left=0.18, x_margin_right=0.32, y_margin=0.06, show_axis=False, save_path=None, save_dpi=300, save_bbox_inches='tight')

Plot a cleaner bipartite network with partition-specific colors, selective labels, grayscale edges, and node sizes based on degree or strength.

Parameters

B : networkx.Graph Bipartite graph with optional 'weight' attributes on edges. group0_nodes : list or set Nodes in the left partition (typically exporters). title : str, optional Title of the plot. figsize : tuple Figure size in inches. exporter_color : str Color for exporter nodes. Default uses Okabe-Ito blue. importer_color : str Color for importer nodes. Default uses Okabe-Ito vermillion. edge_cmap : str Matplotlib colormap for edges. edge_alpha : float Transparency of edges. edge_width_scale : float Maximum width multiplier for edges based on weights. label_top_n : int Number of top exporters and top importers to label. font_size : int Font size for labels. title_font_size : int Font size for the title. node_size_mode : {"strength", "degree", None} Metric used to scale node sizes. If None, all nodes use default_node_size. node_size_range : tuple Minimum and maximum node size when scaling is enabled. default_node_size : int Node size when node_size_mode is None. partition_gap : float Horizontal separation between the two node partitions. label_offset : float Horizontal offset used to place labels outside the nodes. label_min_gap : float Minimum vertical separation enforced between labels on the same side. draw_label_connectors : bool Whether to draw thin connector lines from shifted labels to their nodes. x_margin_left : float Extra horizontal margin on the left side of the plot. x_margin_right : float Extra horizontal margin on the right side of the plot. y_margin : float Extra vertical margin around the layout. show_axis : bool Whether to display axes. save_path : str or None If provided, save the figure to this path. save_dpi : int Resolution used when saving the figure. save_bbox_inches : str Bounding box option passed to savefig.

Returns

matplotlib.axes.Axes The matplotlib Axes object of the plot.

Source code in faonet/plots.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
def plot_bipartite_network_enhanced(
    B,
    group0_nodes,
    title=None,
    figsize=(14, 9),
    exporter_color="#0072B2",
    importer_color="#D55E00",
    edge_cmap="Greys",
    edge_alpha=0.45,
    edge_width_scale=4.0,
    label_top_n=10,
    font_size=13,
    title_font_size=18,
    node_size_mode="strength",
    node_size_range=(250, 1400),
    default_node_size=700,
    partition_gap=1.2,
    label_offset=0.04,
    label_min_gap=0.055,
    draw_label_connectors=True,
    x_margin_left=0.18,
    x_margin_right=0.32,
    y_margin=0.06,
    show_axis=False,
    save_path=None,
    save_dpi=300,
    save_bbox_inches="tight",
):
    """
    Plot a cleaner bipartite network with partition-specific colors, selective labels,
    grayscale edges, and node sizes based on degree or strength.

    Parameters
    ----------
    B : networkx.Graph
        Bipartite graph with optional 'weight' attributes on edges.
    group0_nodes : list or set
        Nodes in the left partition (typically exporters).
    title : str, optional
        Title of the plot.
    figsize : tuple
        Figure size in inches.
    exporter_color : str
        Color for exporter nodes. Default uses Okabe-Ito blue.
    importer_color : str
        Color for importer nodes. Default uses Okabe-Ito vermillion.
    edge_cmap : str
        Matplotlib colormap for edges.
    edge_alpha : float
        Transparency of edges.
    edge_width_scale : float
        Maximum width multiplier for edges based on weights.
    label_top_n : int
        Number of top exporters and top importers to label.
    font_size : int
        Font size for labels.
    title_font_size : int
        Font size for the title.
    node_size_mode : {"strength", "degree", None}
        Metric used to scale node sizes. If None, all nodes use `default_node_size`.
    node_size_range : tuple
        Minimum and maximum node size when scaling is enabled.
    default_node_size : int
        Node size when `node_size_mode` is None.
    partition_gap : float
        Horizontal separation between the two node partitions.
    label_offset : float
        Horizontal offset used to place labels outside the nodes.
    label_min_gap : float
        Minimum vertical separation enforced between labels on the same side.
    draw_label_connectors : bool
        Whether to draw thin connector lines from shifted labels to their nodes.
    x_margin_left : float
        Extra horizontal margin on the left side of the plot.
    x_margin_right : float
        Extra horizontal margin on the right side of the plot.
    y_margin : float
        Extra vertical margin around the layout.
    show_axis : bool
        Whether to display axes.
    save_path : str or None
        If provided, save the figure to this path.
    save_dpi : int
        Resolution used when saving the figure.
    save_bbox_inches : str
        Bounding box option passed to `savefig`.

    Returns
    -------
    matplotlib.axes.Axes
        The matplotlib Axes object of the plot.
    """
    fig, ax = plt.subplots(figsize=figsize)

    group0_nodes = list(group0_nodes)
    group0_set = set(group0_nodes)
    group1_nodes = [node for node in B.nodes if node not in group0_set]

    pos = nx.bipartite_layout(B, group0_nodes)
    pos = {
        node: (
            -partition_gap / 2 if node in group0_set else partition_gap / 2,
            coords[1],
        )
        for node, coords in pos.items()
    }

    edge_data = list(B.edges(data=True))
    weights = [data.get("weight", 1.0) for _, _, data in edge_data]
    max_weight = max(weights) if weights else 1.0
    edge_widths = [0.5 + (weight / max_weight) * edge_width_scale for weight in weights]

    degree_dict = dict(B.degree())
    strength_dict = dict(B.degree(weight="weight"))

    if node_size_mode == "degree":
        size_metric = degree_dict
    elif node_size_mode == "strength":
        size_metric = strength_dict
    else:
        size_metric = None

    if size_metric:
        metric_values = np.array(list(size_metric.values()), dtype=float)
        metric_min = metric_values.min()
        metric_max = metric_values.max()

        if metric_max == metric_min:
            uniform_size = float(np.mean(node_size_range))
            node_sizes = {node: uniform_size for node in B.nodes}
        else:
            low, high = node_size_range
            node_sizes = {
                node: low + (size_metric[node] - metric_min) * (high - low) / (metric_max - metric_min)
                for node in B.nodes
            }
    else:
        node_sizes = {node: default_node_size for node in B.nodes}

    nx.draw_networkx_edges(
        B,
        pos,
        ax=ax,
        edge_color=weights if weights else "0.7",
        edge_cmap=plt.get_cmap(edge_cmap),
        width=edge_widths,
        alpha=edge_alpha,
    )

    nx.draw_networkx_nodes(
        B,
        pos,
        nodelist=group0_nodes,
        node_color=exporter_color,
        node_size=[node_sizes[node] for node in group0_nodes],
        ax=ax,
        linewidths=0.8,
        edgecolors="white",
    )

    nx.draw_networkx_nodes(
        B,
        pos,
        nodelist=group1_nodes,
        node_color=importer_color,
        node_size=[node_sizes[node] for node in group1_nodes],
        ax=ax,
        linewidths=0.8,
        edgecolors="white",
    )

    ranking_metric = size_metric if size_metric is not None else degree_dict
    top_exporters = sorted(group0_nodes, key=lambda node: ranking_metric[node], reverse=True)[:label_top_n]
    top_importers = sorted(group1_nodes, key=lambda node: ranking_metric[node], reverse=True)[:label_top_n]

    def _spread_label_positions(nodes):
        if not nodes:
            return {}

        sorted_nodes = sorted(nodes, key=lambda node: pos[node][1])
        label_y = {}
        previous_y = None

        for node in sorted_nodes:
            current_y = pos[node][1]
            if previous_y is None:
                adjusted_y = current_y
            else:
                adjusted_y = max(current_y, previous_y + label_min_gap)
            label_y[node] = adjusted_y
            previous_y = adjusted_y

        top_limit = max(y_values) if y_values else None
        bottom_limit = min(y_values) if y_values else None

        if top_limit is not None and sorted_nodes:
            overflow = label_y[sorted_nodes[-1]] - top_limit
            if overflow > 0:
                for node in sorted_nodes:
                    label_y[node] -= overflow

        if bottom_limit is not None and sorted_nodes:
            underflow = bottom_limit - label_y[sorted_nodes[0]]
            if underflow > 0:
                for node in sorted_nodes:
                    label_y[node] += underflow

        return label_y

    y_values = [coords[1] for coords in pos.values()]
    exporter_label_y = _spread_label_positions(top_exporters)
    importer_label_y = _spread_label_positions(top_importers)

    for node in top_exporters:
        x, y = pos[node]
        label_y = exporter_label_y[node]
        ax.text(
            x - label_offset,
            label_y,
            str(node),
            fontsize=font_size,
            color="black",
            ha="right",
            va="center",
            clip_on=False,
        )
        if draw_label_connectors and abs(label_y - y) > 1e-9:
            ax.plot(
                [x - 0.01, x - label_offset + 0.01],
                [y, label_y],
                color="0.5",
                linewidth=0.8,
                alpha=0.8,
                solid_capstyle="round",
                zorder=1,
            )

    for node in top_importers:
        x, y = pos[node]
        label_y = importer_label_y[node]
        ax.text(
            x + label_offset,
            label_y,
            str(node),
            fontsize=font_size,
            color="black",
            ha="left",
            va="center",
            clip_on=False,
        )
        if draw_label_connectors and abs(label_y - y) > 1e-9:
            ax.plot(
                [x + 0.01, x + label_offset - 0.01],
                [y, label_y],
                color="0.5",
                linewidth=0.8,
                alpha=0.8,
                solid_capstyle="round",
                zorder=1,
            )

    ax.set_xlim(-partition_gap / 2 - x_margin_left, partition_gap / 2 + x_margin_right)

    if y_values:
        ax.set_ylim(min(y_values) - y_margin, max(y_values) + y_margin)

    if title:
        ax.set_title(title, fontsize=title_font_size, pad=16)

    if not show_axis:
        ax.set_axis_off()

    plt.tight_layout()

    if save_path is not None:
        fig.savefig(save_path, dpi=save_dpi, bbox_inches=save_bbox_inches)

    return ax

plot_clustering_ratio_multiyear(clustering_data, years=None, degree_col='degree', ratio_col='C4_rate', type_col='tipo', exporter_label='Exporter', importer_label='Importer', figsize=(16, 6), colors=None, marker='o', linewidth=2.2, markersize=5.5, alpha=0.95, use_log_x=False, use_log_y=False, suptitle=None, exporter_title='Exporters', importer_title='Importers', xlabel='Degree', ylabel='Mean weighted/unweighted clustering ratio', legend_title='Year', title_font_size=18, panel_title_font_size=15, label_font_size=13, tick_font_size=11, legend_font_size=11, grid_alpha=0.25, save_path=None, save_dpi=300, save_bbox_inches='tight')

Plot the mean clustering ratio versus degree for multiple years in two panels: exporters on the left and importers on the right.

Parameters

clustering_data : dict Dictionary mapping year -> DataFrame produced by compute_bipartite_clustering. years : list, optional Ordered list of years to plot. If None, uses the sorted keys of clustering_data. degree_col : str Column name with node degree. ratio_col : str Column name with clustering ratio. type_col : str Column name indicating node type. exporter_label : str Label used to identify exporter rows in type_col. importer_label : str Label used to identify importer rows in type_col. figsize : tuple Figure size in inches. colors : list or None List of colors to use for the yearly curves. marker : str Marker style for the lines. linewidth : float Line width for the curves. markersize : float Marker size for the curves. alpha : float Transparency level for the curves. use_log_x : bool Whether to use logarithmic scaling on the x-axis. use_log_y : bool Whether to use logarithmic scaling on the y-axis. suptitle : str, optional Figure-level title. exporter_title : str Title of the exporter panel. importer_title : str Title of the importer panel. xlabel : str Label for the x-axis. ylabel : str Label for the y-axis. legend_title : str Title for the legend. title_font_size : int Font size for the figure title. panel_title_font_size : int Font size for panel titles. label_font_size : int Font size for axis labels. tick_font_size : int Font size for axis tick labels. legend_font_size : int Font size for the legend. grid_alpha : float Transparency of the grid lines. save_path : str or None If provided, save the figure to this path. save_dpi : int Resolution used when saving the figure. save_bbox_inches : str Bounding box option passed to savefig.

Returns

tuple (fig, axes) where axes contains the two subplot axes.

Source code in faonet/plots.py
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
def plot_clustering_ratio_multiyear(
    clustering_data,
    years=None,
    degree_col="degree",
    ratio_col="C4_rate",
    type_col="tipo",
    exporter_label="Exporter",
    importer_label="Importer",
    figsize=(16, 6),
    colors=None,
    marker="o",
    linewidth=2.2,
    markersize=5.5,
    alpha=0.95,
    use_log_x=False,
    use_log_y=False,
    suptitle=None,
    exporter_title="Exporters",
    importer_title="Importers",
    xlabel="Degree",
    ylabel="Mean weighted/unweighted clustering ratio",
    legend_title="Year",
    title_font_size=18,
    panel_title_font_size=15,
    label_font_size=13,
    tick_font_size=11,
    legend_font_size=11,
    grid_alpha=0.25,
    save_path=None,
    save_dpi=300,
    save_bbox_inches="tight",
):
    """
    Plot the mean clustering ratio versus degree for multiple years in two panels:
    exporters on the left and importers on the right.

    Parameters
    ----------
    clustering_data : dict
        Dictionary mapping year -> DataFrame produced by `compute_bipartite_clustering`.
    years : list, optional
        Ordered list of years to plot. If None, uses the sorted keys of `clustering_data`.
    degree_col : str
        Column name with node degree.
    ratio_col : str
        Column name with clustering ratio.
    type_col : str
        Column name indicating node type.
    exporter_label : str
        Label used to identify exporter rows in `type_col`.
    importer_label : str
        Label used to identify importer rows in `type_col`.
    figsize : tuple
        Figure size in inches.
    colors : list or None
        List of colors to use for the yearly curves.
    marker : str
        Marker style for the lines.
    linewidth : float
        Line width for the curves.
    markersize : float
        Marker size for the curves.
    alpha : float
        Transparency level for the curves.
    use_log_x : bool
        Whether to use logarithmic scaling on the x-axis.
    use_log_y : bool
        Whether to use logarithmic scaling on the y-axis.
    suptitle : str, optional
        Figure-level title.
    exporter_title : str
        Title of the exporter panel.
    importer_title : str
        Title of the importer panel.
    xlabel : str
        Label for the x-axis.
    ylabel : str
        Label for the y-axis.
    legend_title : str
        Title for the legend.
    title_font_size : int
        Font size for the figure title.
    panel_title_font_size : int
        Font size for panel titles.
    label_font_size : int
        Font size for axis labels.
    tick_font_size : int
        Font size for axis tick labels.
    legend_font_size : int
        Font size for the legend.
    grid_alpha : float
        Transparency of the grid lines.
    save_path : str or None
        If provided, save the figure to this path.
    save_dpi : int
        Resolution used when saving the figure.
    save_bbox_inches : str
        Bounding box option passed to `savefig`.

    Returns
    -------
    tuple
        (fig, axes) where axes contains the two subplot axes.
    """
    if years is None:
        years = sorted(clustering_data)

    if not years:
        raise ValueError("No years were provided in clustering_data.")

    if colors is None:
        colors = ["#0072B2", "#E69F00", "#009E73", "#CC79A7", "#56B4E9", "#000000"]

    fig, axes = plt.subplots(1, 2, figsize=figsize, sharey=False)
    panel_specs = [
        (axes[0], exporter_label, exporter_title),
        (axes[1], importer_label, importer_title),
    ]

    for ax, node_type, panel_title in panel_specs:
        for idx, year in enumerate(years):
            if year not in clustering_data:
                continue

            df_year = clustering_data[year]
            subset = df_year[df_year[type_col] == node_type].copy()
            grouped = (
                subset.groupby(degree_col)[ratio_col]
                .mean()
                .reset_index()
                .sort_values(by=degree_col)
            )

            if grouped.empty:
                continue

            ax.plot(
                grouped[degree_col],
                grouped[ratio_col],
                marker=marker,
                linewidth=linewidth,
                markersize=markersize,
                alpha=alpha,
                color=colors[idx % len(colors)],
                label=str(year),
            )

        if use_log_x:
            ax.set_xscale("log")
        if use_log_y:
            ax.set_yscale("log")

        ax.set_title(panel_title, fontsize=panel_title_font_size, pad=10)
        ax.set_xlabel(xlabel, fontsize=label_font_size)
        ax.set_ylabel(ylabel, fontsize=label_font_size)
        ax.tick_params(axis="both", labelsize=tick_font_size)
        ax.grid(True, which="major", alpha=grid_alpha)
        ax.legend(title=legend_title, fontsize=legend_font_size, title_fontsize=legend_font_size)

    if suptitle:
        fig.suptitle(suptitle, fontsize=title_font_size, y=1.02)

    plt.tight_layout()

    if save_path is not None:
        fig.savefig(save_path, dpi=save_dpi, bbox_inches=save_bbox_inches)

    return fig, axes

plot_degree_bar(df, country_col='Reporter Country', degree_col='Degree', title='Node Degree', xlabel='Country', ylabel='Degree', color='blue', alpha=0.7, figsize=(12, 6), rotation=90)

Plot a bar chart of node degrees (e.g., exporters or importers) in a bipartite network.

Parameters

df : pandas.DataFrame DataFrame containing node information with degree values. country_col : str Name of the column with country or node names. degree_col : str Name of the column with degree values. title : str Title of the plot. xlabel : str Label for the x-axis. ylabel : str Label for the y-axis. color : str Color used for the bars. alpha : float Transparency level for the bars (0 to 1). figsize : tuple Size of the figure in inches (width, height). rotation : int Rotation angle of the x-axis tick labels.

Returns

matplotlib.axes.Axes Axes object of the created plot.

Source code in faonet/plots.py
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
def plot_degree_bar(df, country_col="Reporter Country", degree_col="Degree", 
                    title="Node Degree", xlabel="Country", ylabel="Degree", 
                    color="blue", alpha=0.7, figsize=(12, 6), rotation=90):
    """
    Plot a bar chart of node degrees (e.g., exporters or importers) in a bipartite network.

    Parameters
    ----------
    df : pandas.DataFrame
        DataFrame containing node information with degree values.
    country_col : str
        Name of the column with country or node names.
    degree_col : str
        Name of the column with degree values.
    title : str
        Title of the plot.
    xlabel : str
        Label for the x-axis.
    ylabel : str
        Label for the y-axis.
    color : str
        Color used for the bars.
    alpha : float
        Transparency level for the bars (0 to 1).
    figsize : tuple
        Size of the figure in inches (width, height).
    rotation : int
        Rotation angle of the x-axis tick labels.

    Returns
    -------
    matplotlib.axes.Axes
        Axes object of the created plot.
    """
    df_sorted = df.sort_values(by=degree_col, ascending=False)

    fig, ax = plt.subplots(figsize=figsize)
    ax.bar(df_sorted[country_col], df_sorted[degree_col], color=color, alpha=alpha)

    ax.set_xlabel(xlabel)
    ax.set_ylabel(ylabel)
    ax.set_title(title)
    ax.tick_params(axis='x', rotation=rotation)
    plt.tight_layout()
    plt.show()

    return ax

plot_degree_by_rank(df_reporters, df_partners, reporter_label='Exporters', partner_label='Importers', degree_col='Degree', figsize=(10, 5), reporter_color='blue', partner_color='orange', alpha=0.7, use_log_y=True, use_log_x=False, title='Node Degree by Rank', xlabel='Rank', ylabel='Degree (Number of Connections)')

Plot degree values of reporter and partner countries sorted by rank in descending order.

Parameters

df_reporters : pandas.DataFrame DataFrame containing degree values for reporter (exporter) nodes. df_partners : pandas.DataFrame DataFrame containing degree values for partner (importer) nodes. reporter_label : str Label for reporter nodes (used in legend). partner_label : str Label for partner nodes (used in legend). degree_col : str Column name containing degree values. figsize : tuple Size of the figure in inches (width, height). reporter_color : str Color used for reporter points and fit line. partner_color : str Color used for partner points and fit line. alpha : float Transparency level for the scatter points. use_log_y : bool Whether to use log scale for the y-axis. use_log_x : bool Whether to use log scale for the x-axis. title : str Title of the plot. xlabel : str Label for the x-axis. ylabel : str Label for the y-axis.

Returns

matplotlib.axes.Axes The matplotlib Axes object of the plot.

Source code in faonet/plots.py
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
def plot_degree_by_rank(df_reporters, df_partners,
                        reporter_label="Exporters",
                        partner_label="Importers",
                        degree_col="Degree",
                        figsize=(10, 5),
                        reporter_color="blue",
                        partner_color="orange",
                        alpha=0.7,
                        use_log_y=True,
                        use_log_x=False,
                        title="Node Degree by Rank",
                        xlabel="Rank",
                        ylabel="Degree (Number of Connections)"):
    """
    Plot degree values of reporter and partner countries sorted by rank in descending order.

    Parameters
    ----------
    df_reporters : pandas.DataFrame
        DataFrame containing degree values for reporter (exporter) nodes.
    df_partners : pandas.DataFrame
        DataFrame containing degree values for partner (importer) nodes.
    reporter_label : str
        Label for reporter nodes (used in legend).
    partner_label : str
        Label for partner nodes (used in legend).
    degree_col : str
        Column name containing degree values.
    figsize : tuple
        Size of the figure in inches (width, height).
    reporter_color : str
        Color used for reporter points and fit line.
    partner_color : str
        Color used for partner points and fit line.
    alpha : float
        Transparency level for the scatter points.
    use_log_y : bool
        Whether to use log scale for the y-axis.
    use_log_x : bool
        Whether to use log scale for the x-axis.
    title : str
        Title of the plot.
    xlabel : str
        Label for the x-axis.
    ylabel : str
        Label for the y-axis.

    Returns
    -------
    matplotlib.axes.Axes
        The matplotlib Axes object of the plot.
    """
    df_reporters_sorted = df_reporters.sort_values(by=degree_col, ascending=False)
    df_partners_sorted = df_partners.sort_values(by=degree_col, ascending=False)

    fig, ax = plt.subplots(figsize=figsize)

    ax.scatter(range(1, len(df_reporters_sorted) + 1),
               df_reporters_sorted[degree_col],
               color=reporter_color,
               label=reporter_label,
               alpha=alpha)

    ax.scatter(range(1, len(df_partners_sorted) + 1),
               df_partners_sorted[degree_col],
               color=partner_color,
               label=partner_label,
               alpha=alpha)

    if use_log_y:
        ax.set_yscale("log")
    if use_log_x:
        ax.set_xscale("log")

    ax.set_xlabel(xlabel)
    ax.set_ylabel(ylabel)
    ax.set_title(title)
    ax.legend()
    ax.grid(True)
    plt.tight_layout()
    plt.show()

    return ax

plot_degree_comparison(df_reporters, df_partners, reporter_country_col='Reporter Country', partner_country_col='Partner Country', degree_col='Degree', figsize=(12, 10), reporter_color='blue', partner_color='orange', alpha=0.7, rotation=90, use_log_scale=False)

Plot side-by-side scatter plots comparing the degree of reporter and partner countries.

Parameters

df_reporters : pandas.DataFrame DataFrame containing degree values for reporter (exporter) nodes. df_partners : pandas.DataFrame DataFrame containing degree values for partner (importer) nodes. reporter_country_col : str Column name for reporter (exporter) country names. partner_country_col : str Column name for partner (importer) country names. degree_col : str Column name containing the degree values. figsize : tuple Size of the entire figure in inches (width, height). reporter_color : str Color used for the reporter scatter plot. partner_color : str Color used for the partner scatter plot. alpha : float Transparency level for the scatter points. rotation : int Rotation angle for x-axis tick labels. use_log_scale : bool If True, apply logarithmic scale to the y-axis.

Returns

matplotlib.figure.Figure The matplotlib Figure object containing the two subplots.

Source code in faonet/plots.py
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
def plot_degree_comparison(df_reporters, df_partners,
                           reporter_country_col="Reporter Country",
                           partner_country_col="Partner Country",
                           degree_col="Degree",
                           figsize=(12, 10),
                           reporter_color="blue",
                           partner_color="orange",
                           alpha=0.7,
                           rotation=90,
                           use_log_scale=False):
    """
    Plot side-by-side scatter plots comparing the degree of reporter and partner countries.

    Parameters
    ----------
    df_reporters : pandas.DataFrame
        DataFrame containing degree values for reporter (exporter) nodes.
    df_partners : pandas.DataFrame
        DataFrame containing degree values for partner (importer) nodes.
    reporter_country_col : str
        Column name for reporter (exporter) country names.
    partner_country_col : str
        Column name for partner (importer) country names.
    degree_col : str
        Column name containing the degree values.
    figsize : tuple
        Size of the entire figure in inches (width, height).
    reporter_color : str
        Color used for the reporter scatter plot.
    partner_color : str
        Color used for the partner scatter plot.
    alpha : float
        Transparency level for the scatter points.
    rotation : int
        Rotation angle for x-axis tick labels.
    use_log_scale : bool
        If True, apply logarithmic scale to the y-axis.

    Returns
    -------
    matplotlib.figure.Figure
        The matplotlib Figure object containing the two subplots.
    """
    df_reporters_sorted = df_reporters.sort_values(by=degree_col, ascending=False)
    df_partners_sorted = df_partners.sort_values(by=degree_col, ascending=False)

    fig, axs = plt.subplots(1, 2, figsize=figsize, sharey=True)

    axs[0].scatter(df_reporters_sorted[reporter_country_col],
                   df_reporters_sorted[degree_col],
                   color=reporter_color, alpha=alpha)
    axs[0].set_xlabel("Exporter Countries")
    axs[0].set_ylabel("Degree (Number of Connections)")
    axs[0].set_title("Degree - Reporter Countries")
    axs[0].tick_params(axis='x', rotation=rotation)
    if use_log_scale:
        axs[0].set_yscale('log')

    axs[1].scatter(df_partners_sorted[partner_country_col],
                   df_partners_sorted[degree_col],
                   color=partner_color, alpha=alpha)
    axs[1].set_xlabel("Importer Countries")
    axs[1].set_title("Degree - Partner Countries")
    axs[1].tick_params(axis='x', rotation=rotation)
    if use_log_scale:
        axs[1].set_yscale('log')

    plt.tight_layout()
    plt.show()
    return

plot_degree_rank_multiyear(exporter_data, importer_data, years=None, degree_col='Degree', figsize=(16, 6), colors=None, marker='o', linewidth=2.2, markersize=5.5, alpha=0.95, use_log_y=True, use_log_x=False, exporter_title='Exporters', importer_title='Importers', suptitle=None, xlabel='Rank', ylabel='Degree', legend_title='Year', title_font_size=18, panel_title_font_size=15, label_font_size=13, tick_font_size=11, legend_font_size=11, grid_alpha=0.25, save_path=None, save_dpi=300, save_bbox_inches='tight')

Plot rank-degree curves for multiple years in two side-by-side panels: exporters on the left and importers on the right.

Parameters

exporter_data : dict Dictionary mapping year -> DataFrame for exporter nodes. importer_data : dict Dictionary mapping year -> DataFrame for importer nodes. years : list, optional Ordered list of years to plot. If None, uses the sorted intersection of years present in both dictionaries. degree_col : str Column name containing the degree-like metric to plot. figsize : tuple Figure size in inches. colors : list or None List of colors to use for the yearly curves. marker : str Marker style for the lines. linewidth : float Line width for the curves. markersize : float Marker size for the curves. alpha : float Transparency level for the curves. use_log_y : bool Whether to use logarithmic scaling on the y-axis. use_log_x : bool Whether to use logarithmic scaling on the x-axis. exporter_title : str Title of the exporter panel. importer_title : str Title of the importer panel. suptitle : str, optional Figure-level title. xlabel : str Label for the x-axis. ylabel : str Label for the y-axis. legend_title : str Title for the legend. title_font_size : int Font size for the figure title. panel_title_font_size : int Font size for panel titles. label_font_size : int Font size for axis labels. tick_font_size : int Font size for axis tick labels. legend_font_size : int Font size for the legend. grid_alpha : float Transparency of the grid lines. save_path : str or None If provided, save the figure to this path. save_dpi : int Resolution used when saving the figure. save_bbox_inches : str Bounding box option passed to savefig.

Returns

tuple (fig, axes) where axes contains the two subplot axes.

Source code in faonet/plots.py
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
def plot_degree_rank_multiyear(
    exporter_data,
    importer_data,
    years=None,
    degree_col="Degree",
    figsize=(16, 6),
    colors=None,
    marker="o",
    linewidth=2.2,
    markersize=5.5,
    alpha=0.95,
    use_log_y=True,
    use_log_x=False,
    exporter_title="Exporters",
    importer_title="Importers",
    suptitle=None,
    xlabel="Rank",
    ylabel="Degree",
    legend_title="Year",
    title_font_size=18,
    panel_title_font_size=15,
    label_font_size=13,
    tick_font_size=11,
    legend_font_size=11,
    grid_alpha=0.25,
    save_path=None,
    save_dpi=300,
    save_bbox_inches="tight",
):
    """
    Plot rank-degree curves for multiple years in two side-by-side panels:
    exporters on the left and importers on the right.

    Parameters
    ----------
    exporter_data : dict
        Dictionary mapping year -> DataFrame for exporter nodes.
    importer_data : dict
        Dictionary mapping year -> DataFrame for importer nodes.
    years : list, optional
        Ordered list of years to plot. If None, uses the sorted intersection
        of years present in both dictionaries.
    degree_col : str
        Column name containing the degree-like metric to plot.
    figsize : tuple
        Figure size in inches.
    colors : list or None
        List of colors to use for the yearly curves.
    marker : str
        Marker style for the lines.
    linewidth : float
        Line width for the curves.
    markersize : float
        Marker size for the curves.
    alpha : float
        Transparency level for the curves.
    use_log_y : bool
        Whether to use logarithmic scaling on the y-axis.
    use_log_x : bool
        Whether to use logarithmic scaling on the x-axis.
    exporter_title : str
        Title of the exporter panel.
    importer_title : str
        Title of the importer panel.
    suptitle : str, optional
        Figure-level title.
    xlabel : str
        Label for the x-axis.
    ylabel : str
        Label for the y-axis.
    legend_title : str
        Title for the legend.
    title_font_size : int
        Font size for the figure title.
    panel_title_font_size : int
        Font size for panel titles.
    label_font_size : int
        Font size for axis labels.
    tick_font_size : int
        Font size for axis tick labels.
    legend_font_size : int
        Font size for the legend.
    grid_alpha : float
        Transparency of the grid lines.
    save_path : str or None
        If provided, save the figure to this path.
    save_dpi : int
        Resolution used when saving the figure.
    save_bbox_inches : str
        Bounding box option passed to `savefig`.

    Returns
    -------
    tuple
        (fig, axes) where axes contains the two subplot axes.
    """
    if years is None:
        years = sorted(set(exporter_data).intersection(importer_data))

    if not years:
        raise ValueError("No overlapping years were provided in exporter_data and importer_data.")

    if colors is None:
        colors = ["#0072B2", "#E69F00", "#009E73", "#CC79A7", "#56B4E9", "#000000"]

    fig, axes = plt.subplots(1, 2, figsize=figsize, sharey=False)

    panel_specs = [
        (axes[0], exporter_data, exporter_title),
        (axes[1], importer_data, importer_title),
    ]

    for ax, data_dict, panel_title in panel_specs:
        for idx, year in enumerate(years):
            if year not in data_dict:
                continue

            df_sorted = data_dict[year].sort_values(by=degree_col, ascending=False).reset_index(drop=True)
            ranks = range(1, len(df_sorted) + 1)

            ax.plot(
                ranks,
                df_sorted[degree_col],
                marker=marker,
                linewidth=linewidth,
                markersize=markersize,
                alpha=alpha,
                color=colors[idx % len(colors)],
                label=str(year),
            )

        if use_log_y:
            ax.set_yscale("log")
        if use_log_x:
            ax.set_xscale("log")

        ax.set_title(panel_title, fontsize=panel_title_font_size, pad=10)
        ax.set_xlabel(xlabel, fontsize=label_font_size)
        ax.set_ylabel(ylabel, fontsize=label_font_size)
        ax.tick_params(axis="both", labelsize=tick_font_size)
        ax.grid(True, which="major", alpha=grid_alpha)
        ax.legend(title=legend_title, fontsize=legend_font_size, title_fontsize=legend_font_size)

    if suptitle:
        fig.suptitle(suptitle, fontsize=title_font_size, y=1.02)

    plt.tight_layout()

    if save_path is not None:
        fig.savefig(save_path, dpi=save_dpi, bbox_inches=save_bbox_inches)

    return fig, axes

plot_mean_clustering_ratio_vs_degree(df, degree_col='degree', ratio_col='C4_rate', type_col='tipo', node_col='node', show_labels=False, label_max_names_per_line=3, label_font_size=6, exporter_label='Exporters', importer_label='Importers', save_path=None, save_dpi=300, save_bbox_inches='tight')

Plot the mean clustering ratio ⟨C4b^w / C4b⟩ versus node degree for each node type.

The function groups nodes by degree and computes the average clustering ratio per group, optionally displaying node labels.

Parameters

df : pandas.DataFrame DataFrame containing at least the clustering ratio, node degree, type and identifier columns. degree_col : str Column name with node degrees. ratio_col : str Column name with clustering ratio (e.g., C4b^w / C4b). type_col : str Column name indicating node type (e.g., 'Exporter' or 'Importer'). node_col : str Column name with node identifiers (used for optional annotations). show_labels : bool Whether to annotate each point with its corresponding node names. label_max_names_per_line : int Maximum number of node names to place on each line of an annotation. label_font_size : int Font size used for annotations. exporter_label : str Label used in the legend for exporter nodes. importer_label : str Label used in the legend for importer nodes. save_path : str or None If provided, save the figure to this path. save_dpi : int Resolution used when saving the figure. save_bbox_inches : str Bounding box option passed to savefig.

Returns

matplotlib.axes.Axes The matplotlib Axes object of the plot.

Source code in faonet/plots.py
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
def plot_mean_clustering_ratio_vs_degree(
    df,
    degree_col="degree",
    ratio_col="C4_rate",
    type_col="tipo",
    node_col="node",
    show_labels=False,
    label_max_names_per_line=3,
    label_font_size=6,
    exporter_label="Exporters",
    importer_label="Importers",
    save_path=None,
    save_dpi=300,
    save_bbox_inches="tight",
):
    """
    Plot the mean clustering ratio ⟨C4b^w / C4b⟩ versus node degree for each node type.

    The function groups nodes by degree and computes the average clustering ratio per group,
    optionally displaying node labels.

    Parameters
    ----------
    df : pandas.DataFrame
        DataFrame containing at least the clustering ratio, node degree, type and identifier columns.
    degree_col : str
        Column name with node degrees.
    ratio_col : str
        Column name with clustering ratio (e.g., C4b^w / C4b).
    type_col : str
        Column name indicating node type (e.g., 'Exporter' or 'Importer').
    node_col : str
        Column name with node identifiers (used for optional annotations).
    show_labels : bool
        Whether to annotate each point with its corresponding node names.
    label_max_names_per_line : int
        Maximum number of node names to place on each line of an annotation.
    label_font_size : int
        Font size used for annotations.
    exporter_label : str
        Label used in the legend for exporter nodes.
    importer_label : str
        Label used in the legend for importer nodes.
    save_path : str or None
        If provided, save the figure to this path.
    save_dpi : int
        Resolution used when saving the figure.
    save_bbox_inches : str
        Bounding box option passed to `savefig`.

    Returns
    -------
    matplotlib.axes.Axes
        The matplotlib Axes object of the plot.
    """
    # Group by type and degree
    grouped = (
        df.groupby([type_col, degree_col])
        .agg({
            ratio_col: "mean",
            node_col: lambda x: ', '.join(x)
        })
        .reset_index()
        .rename(columns={node_col: "nodos"})
    )

    # Create figure
    fig, ax = plt.subplots(figsize=(10, 6))

    # Plot each type separately
    for tipo in grouped[type_col].unique():
        subset = grouped[grouped[type_col] == tipo]
        legend_label = exporter_label if tipo.lower().startswith("export") else importer_label
        ax.plot(subset[degree_col], subset[ratio_col],
                label=legend_label,
                marker='o' if tipo.lower().startswith("export") else 's',
                linestyle='-')

        if show_labels:
            x_offset = -8 if tipo.lower().startswith("export") else 8
            y_offset = 6 if tipo.lower().startswith("export") else -6
            for _, row in subset.iterrows():
                node_names = [name.strip() for name in row["nodos"].split(",") if name.strip()]
                label_lines = [
                    ", ".join(node_names[i:i + label_max_names_per_line])
                    for i in range(0, len(node_names), label_max_names_per_line)
                ]
                label_text = "\n".join(label_lines)
                ax.annotate(
                    label_text,
                    (row[degree_col], row[ratio_col]),
                    xytext=(x_offset, y_offset),
                    textcoords="offset points",
                    fontsize=label_font_size,
                    ha="right" if tipo.lower().startswith("export") else "left",
                    va="bottom" if tipo.lower().startswith("export") else "top",
                )

    # Labels and styling
    ax.set_xlabel("Degree")
    ax.set_ylabel("⟨C4b^w / C4b⟩")
    ax.set_title("Mean clustering ratio ⟨C4b^w / C4b⟩ vs. Degree by node type")
    ax.grid(True)
    ax.legend()
    plt.tight_layout()

    if save_path is not None:
        fig.savefig(save_path, dpi=save_dpi, bbox_inches=save_bbox_inches)

    return ax

plot_top_betweenness(df, col, title=None, color='steelblue', top_n=10, label_col='node', xlabel='Betweenness Centrality')

Plot a horizontal bar chart of the top N nodes ranked by betweenness centrality.

Parameters

df : pandas.DataFrame DataFrame containing betweenness values and node labels. col : str Column name containing betweenness centrality scores. title : str, optional Title of the plot (default is None). color : str Color of the bars in the chart. top_n : int Number of top-ranking nodes to display. label_col : str Column name with node identifiers (default is 'node'). xlabel : str Label for the x-axis.

Returns

matplotlib.axes.Axes The matplotlib Axes object of the plot.

Source code in faonet/plots.py
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
def plot_top_betweenness(df, col, title=None, color="steelblue", top_n=10, label_col="node", xlabel="Betweenness Centrality"):
    """
    Plot a horizontal bar chart of the top N nodes ranked by betweenness centrality.

    Parameters
    ----------
    df : pandas.DataFrame
        DataFrame containing betweenness values and node labels.
    col : str
        Column name containing betweenness centrality scores.
    title : str, optional
        Title of the plot (default is None).
    color : str
        Color of the bars in the chart.
    top_n : int
        Number of top-ranking nodes to display.
    label_col : str
        Column name with node identifiers (default is 'node').
    xlabel : str
        Label for the x-axis.

    Returns
    -------
    matplotlib.axes.Axes
        The matplotlib Axes object of the plot.
    """
    top = df.sort_values(by=col, ascending=False).head(top_n)

    fig, ax = plt.subplots(figsize=(10, 6))
    ax.barh(top[label_col], top[col], color=color)
    ax.set_xlabel(xlabel)
    ax.set_title(title or f"Top {top_n} Nodes by {col}")
    ax.invert_yaxis()
    plt.tight_layout()
    plt.show()

    return ax

plot_trade_scatter(df, x_col='Reporter Country Code (M49)', y_col='Partner Country Code (M49)', value_col='Value', step=10, cmap='viridis', alpha=0.8, figsize=(8, 6))

Plot a scatter plot of trade interactions between reporter and partner countries.

Parameters

df : pd.DataFrame DataFrame containing trade data. x_col : str Column name for x-axis (e.g. reporter country codes). y_col : str Column name for y-axis (e.g. partner country codes). value_col : str Column name used for point color intensity (e.g. trade value). step : int Interval of tick marks on the axes (e.g. show every 10th value). cmap : str Colormap to use for the scatter points. alpha : float Transparency level for the points. figsize : tuple Figure size in inches.

Returns

matplotlib.axes.Axes The plot axes object.

Source code in faonet/plots.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def plot_trade_scatter(df, x_col='Reporter Country Code (M49)', y_col='Partner Country Code (M49)', 
                       value_col='Value', step=10, cmap='viridis', alpha=0.8, figsize=(8, 6)):
    """
    Plot a scatter plot of trade interactions between reporter and partner countries.

    Parameters
    ----------
    df : pd.DataFrame
        DataFrame containing trade data.
    x_col : str
        Column name for x-axis (e.g. reporter country codes).
    y_col : str
        Column name for y-axis (e.g. partner country codes).
    value_col : str
        Column name used for point color intensity (e.g. trade value).
    step : int
        Interval of tick marks on the axes (e.g. show every 10th value).
    cmap : str
        Colormap to use for the scatter points.
    alpha : float
        Transparency level for the points.
    figsize : tuple
        Figure size in inches.

    Returns
    -------
    matplotlib.axes.Axes
        The plot axes object.
    """
    ax = df.plot(kind='scatter', x=x_col, y=y_col, s=32, c=value_col, 
                 cmap=cmap, alpha=alpha, figsize=figsize)

    # Define ticks
    x_ticks = df[x_col].unique()
    y_ticks = df[y_col].unique()
    ax.set_xticks(x_ticks[::step])
    ax.set_yticks(y_ticks[::step])

    # Style
    ax.spines[['top', 'right']].set_visible(False)
    plt.tight_layout()
    plt.show()

    return ax

plot_weight_matrix(df, row='Partner Countries', col='Reporter Countries', value='Value', cmap='coolwarm', figsize=(20, 15), title='Weighted Adjacency Matrix (Trade Volume)', save_path=None, save_dpi=300, save_bbox_inches='tight')

Plot a heatmap of the weighted bipartite adjacency matrix.

Parameters

df : pandas.DataFrame DataFrame containing filtered trade data with exporter, importer, and weight columns. row : str Column name to use as rows of the matrix (typically importers). col : str Column name to use as columns of the matrix (typically exporters). value : str Column containing the weight or value of the trade relationship. cmap : str Colormap used for the heatmap. figsize : tuple Size of the figure in inches (width, height). title : str Title of the plot. save_path : str or None If provided, save the figure to this path. save_dpi : int Resolution used when saving the figure. save_bbox_inches : str Bounding box option passed to savefig.

Returns

matplotlib.axes.Axes The Axes object of the resulting heatmap.

Source code in faonet/plots.py
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
def plot_weight_matrix(df, row="Partner Countries", col="Reporter Countries", 
                       value="Value", cmap="coolwarm", figsize=(20, 15), 
                       title="Weighted Adjacency Matrix (Trade Volume)",
                       save_path=None, save_dpi=300, save_bbox_inches="tight"):
    """
    Plot a heatmap of the weighted bipartite adjacency matrix.

    Parameters
    ----------
    df : pandas.DataFrame
        DataFrame containing filtered trade data with exporter, importer, and weight columns.
    row : str
        Column name to use as rows of the matrix (typically importers).
    col : str
        Column name to use as columns of the matrix (typically exporters).
    value : str
        Column containing the weight or value of the trade relationship.
    cmap : str
        Colormap used for the heatmap.
    figsize : tuple
        Size of the figure in inches (width, height).
    title : str
        Title of the plot.
    save_path : str or None
        If provided, save the figure to this path.
    save_dpi : int
        Resolution used when saving the figure.
    save_bbox_inches : str
        Bounding box option passed to `savefig`.

    Returns
    -------
    matplotlib.axes.Axes
        The Axes object of the resulting heatmap.
    """
    # Build matrix
    matrix = df.pivot(index=row, columns=col, values=value)

    # Sort rows/cols by total weights
    matrix = matrix.loc[matrix.sum(axis=1).sort_values(ascending=False).index,
                        matrix.sum(axis=0).sort_values(ascending=False).index]

    # Plot
    fig, ax = plt.subplots(figsize=figsize)
    sns.heatmap(matrix, cmap=cmap, annot=False, linewidths=0.5, ax=ax)

    ax.set_xlabel(col)
    ax.set_ylabel(row)
    ax.set_title(title)
    fig.tight_layout()

    if save_path is not None:
        fig.savefig(save_path, dpi=save_dpi, bbox_inches=save_bbox_inches)

    plt.show()

    return ax

Export Tools

export_gml(G, filepath)

Export a NetworkX graph to a GML file.

Parameters

G : networkx.Graph The graph to be exported. filepath : str Path to the output .gml file.

Source code in faonet/export.py
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def export_gml(G, filepath):
    """
    Export a NetworkX graph to a GML file.

    Parameters
    ----------
    G : networkx.Graph
        The graph to be exported.
    filepath : str
        Path to the output .gml file.
    """
    nx.write_gml(G, filepath)

Fitting

fit_strength_vs_degree(df_exporters, df_importers, degree_col='Degree', strength_col='Strength', figsize=(8, 5), show_plot=True, save_path=None, save_dpi=300, save_bbox_inches='tight')

Fit and plot strength vs. degree in log-log scale for exporters and importers.

Parameters

df_exporters : pandas.DataFrame DataFrame containing exporter degree and strength values. df_importers : pandas.DataFrame DataFrame containing importer degree and strength values. degree_col : str Column name for degree values. strength_col : str Column name for strength values. figsize : tuple Size of the figure in inches. show_plot : bool Whether to display the plot. save_path : str or None If provided, save the figure to this path. save_dpi : int Resolution used when saving the figure. save_bbox_inches : str Bounding box option passed to savefig.

Returns

dict Dictionary containing slopes, intercepts, R² values, and fitted curves for exporters and importers.

Source code in faonet/fitting.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def fit_strength_vs_degree(df_exporters, df_importers,
                           degree_col="Degree", strength_col="Strength",
                           figsize=(8, 5), show_plot=True,
                           save_path=None, save_dpi=300, save_bbox_inches="tight"):
    """
    Fit and plot strength vs. degree in log-log scale for exporters and importers.

    Parameters
    ----------
    df_exporters : pandas.DataFrame
        DataFrame containing exporter degree and strength values.
    df_importers : pandas.DataFrame
        DataFrame containing importer degree and strength values.
    degree_col : str
        Column name for degree values.
    strength_col : str
        Column name for strength values.
    figsize : tuple
        Size of the figure in inches.
    show_plot : bool
        Whether to display the plot.
    save_path : str or None
        If provided, save the figure to this path.
    save_dpi : int
        Resolution used when saving the figure.
    save_bbox_inches : str
        Bounding box option passed to `savefig`.

    Returns
    -------
    dict
        Dictionary containing slopes, intercepts, R² values, and fitted curves
        for exporters and importers.
    """
    def power_law_fit(degree_vals, strength_vals):
        mask = (degree_vals > 0) & (strength_vals > 0)
        degree_vals = degree_vals[mask]
        strength_vals = strength_vals[mask]

        log_degree = np.log10(degree_vals)
        log_strength = np.log10(strength_vals)

        slope, intercept, r_value, _, _ = linregress(log_degree, log_strength)

        sorted_indices = np.argsort(degree_vals)
        degree_sorted = degree_vals[sorted_indices]
        fit_strength = 10 ** intercept * degree_sorted ** slope

        return slope, intercept, r_value**2, degree_sorted, fit_strength

    # Ajustes
    slope_exp, intercept_exp, r2_exp, deg_exp, fit_exp = power_law_fit(
        df_exporters[degree_col].values,
        df_exporters[strength_col].values)

    slope_imp, intercept_imp, r2_imp, deg_imp, fit_imp = power_law_fit(
        df_importers[degree_col].values,
        df_importers[strength_col].values)

    fig = None
    if show_plot or save_path is not None:
        fig, ax = plt.subplots(figsize=figsize)
        ax.set_xscale('log')
        ax.set_yscale('log')
        ax.set_xlabel('Degree (Number of Connections)')
        ax.set_ylabel('Strength (Sum of Weights)')
        ax.set_title('Power-law Fit: Strength vs Degree')
        ax.grid(True)

        # Puntos
        ax.scatter(df_exporters[degree_col], df_exporters[strength_col], 
                   alpha=0.7, color='blue', label=f'Exporters (β={slope_exp:.2f})')
        ax.scatter(df_importers[degree_col], df_importers[strength_col],
                   alpha=0.7, color='orange', label=f'Importers (β={slope_imp:.2f})')

        # Líneas de ajuste
        ax.plot(deg_exp, fit_exp, color='blue', linestyle='dashed')
        ax.plot(deg_imp, fit_imp, color='orange', linestyle='dashed')

        ax.legend()
        fig.tight_layout()

        if save_path is not None:
            fig.savefig(save_path, dpi=save_dpi, bbox_inches=save_bbox_inches)

    if show_plot and fig is not None:
        plt.show()
    elif fig is not None:
        plt.close(fig)

    return {
        "exporters": {
            "slope": slope_exp,
            "intercept": intercept_exp,
            "r_squared": r2_exp,
            "x": deg_exp,
            "fit": fit_exp
        },
        "importers": {
            "slope": slope_imp,
            "intercept": intercept_imp,
            "r_squared": r2_imp,
            "x": deg_imp,
            "fit": fit_imp
        }
    }

fit_truncated_power_law(degrees, title='Truncated Power-Law Fit', xlabel='Degree', ylabel='Frequency', show_plot=True, figsize=(8, 6), color_data='black', color_fit='darkred', save_path=None, save_dpi=300, save_bbox_inches='tight')

Fit a truncated power-law to a degree distribution and optionally plot the result.

Parameters

degrees : array-like Degree values, not yet aggregated into frequencies. title : str Title of the plot. xlabel : str Label for the x-axis. ylabel : str Label for the y-axis. show_plot : bool Whether to display the plot. figsize : tuple Size of the figure in inches. color_data : str Color used for the observed data points. color_fit : str Color used for the fitted curve. save_path : str or None If provided, save the figure to this path. save_dpi : int Resolution used when saving the figure. save_bbox_inches : str Bounding box option passed to savefig.

Returns

dict Dictionary containing fitted parameters, R², observed frequencies, and fitted values.

Source code in faonet/fitting.py
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def fit_truncated_power_law(degrees,
                             title="Truncated Power-Law Fit",
                             xlabel="Degree",
                             ylabel="Frequency",
                             show_plot=True,
                             figsize=(8, 6),
                             color_data="black",
                             color_fit="darkred",
                             save_path=None,
                             save_dpi=300,
                             save_bbox_inches="tight"):
    """
    Fit a truncated power-law to a degree distribution and optionally plot the result.

    Parameters
    ----------
    degrees : array-like
        Degree values, not yet aggregated into frequencies.
    title : str
        Title of the plot.
    xlabel : str
        Label for the x-axis.
    ylabel : str
        Label for the y-axis.
    show_plot : bool
        Whether to display the plot.
    figsize : tuple
        Size of the figure in inches.
    color_data : str
        Color used for the observed data points.
    color_fit : str
        Color used for the fitted curve.
    save_path : str or None
        If provided, save the figure to this path.
    save_dpi : int
        Resolution used when saving the figure.
    save_bbox_inches : str
        Bounding box option passed to `savefig`.

    Returns
    -------
    dict
        Dictionary containing fitted parameters, R², observed frequencies,
        and fitted values.
    """
    degrees = np.asarray(degrees)
    values, counts = np.unique(degrees, return_counts=True)

    # Fit
    popt, _ = curve_fit(truncated_power_law, values, counts, maxfev=10000)
    fit_values = truncated_power_law(values, *popt)
    r2 = r_squared(counts, fit_values)

    fig = None
    if show_plot or save_path is not None:
        fig, ax = plt.subplots(figsize=figsize)
        ax.scatter(values, counts, label="Data", color=color_data)
        ax.plot(values, fit_values, label=f"Fit (R² = {r2:.2f})", color=color_fit)
        ax.set_xlabel(xlabel)
        ax.set_ylabel(ylabel)
        ax.set_title(title)
        ax.legend()
        ax.grid(True)
        fig.tight_layout()

        if save_path is not None:
            fig.savefig(save_path, dpi=save_dpi, bbox_inches=save_bbox_inches)

    if show_plot and fig is not None:
        plt.show()
    elif fig is not None:
        plt.close(fig)

    return {
        "parameters": {"a": popt[0], "b": popt[1], "c": popt[2]},
        "r_squared": r2,
        "x": values,
        "y": counts,
        "fit": fit_values
    }

r_squared(y_true, y_pred)

Compute the coefficient of determination (R²) between observed and predicted values.

Parameters

y_true : array-like Observed data values. y_pred : array-like Fitted or predicted data values.

Returns

float R² value indicating the goodness of fit.

Source code in faonet/fitting.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def r_squared(y_true, y_pred):
    """
    Compute the coefficient of determination (R²) between observed and predicted values.

    Parameters
    ----------
    y_true : array-like
        Observed data values.
    y_pred : array-like
        Fitted or predicted data values.

    Returns
    -------
    float
        R² value indicating the goodness of fit.
    """
    ss_res = np.sum((y_true - y_pred) ** 2)
    ss_tot = np.sum((y_true - np.mean(y_true)) ** 2)
    return 1 - (ss_res / ss_tot)

truncated_power_law(x, a, b, c)

Truncated power-law function.

Parameters

x : array-like Degree values. a : float Scaling factor. b : float Power-law exponent. c : float Cutoff parameter.

Returns

array-like Values computed from the truncated power-law formula: a * x^(-b) * exp(-x/c).

Source code in faonet/fitting.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def truncated_power_law(x, a, b, c):
    """

    Truncated power-law function.

    Parameters
    ----------
    x : array-like
        Degree values.
    a : float
        Scaling factor.
    b : float
        Power-law exponent.
    c : float
        Cutoff parameter.

    Returns
    -------
    array-like
        Values computed from the truncated power-law formula: a * x^(-b) * exp(-x/c).
    """
    return a * np.power(x, -b) * np.exp(-x / c)