Tabular Data Federated Statistics

In this example, we will show how to generate federated statistics for tabular data that can be represented as Pandas Data Frame.

NVIDIA FLARE Installation

for the complete installation instructions, see Installation

pip install nvflare

get the example code from github:

git clone https://github.com/NVIDIA/NVFlare.git

then navigate to the hello-tabular-stats directory:

git switch <release branch>
cd examples/hello-world/hello-tabular-stats

Install the dependency

pip install -r requirements.txt

Install Optional Quantile Dependency – fastdigest

If you intend to calculate quantiles, install fastdigest==0.4.0.

Skip this step if you don’t need quantile statistics.

pip install fastdigest==0.4.0

on Ubuntu, you might get the following error:

Cargo, the Rust package manager, is not installed or is not on PATH.
This package requires Rust and Cargo to compile extensions. Install it through
the system's package manager or via https://rustup.rs/

Checking for Rust toolchain....

This is because fastdigest (or its dependencies) requires Rust and Cargo to build.

You need to install Rust and Cargo on your Ubuntu system. Follow these steps: Install Rust and Cargo Run the following command to install Rust using rustup:

./install_cargo.sh

Then you can install fastdigest again

pip install fastdigest==0.4.0

Code Structure

hello-tabular-stats
|
├── client.py         # client local training script
├── job.py            # job recipe that defines client and server configurations
├── prepare_data.py   # utilities to download data
├── install_cargo.sh  # scripts to install rust and cargo needed for quantil dependency, only needed if you plan to install quantile dependency
└── requirements.txt  # dependencies
├── demo
│   └── visualization.ipynb # Visualization Notebook

Data

In this example, we are using the UCI (University of California, Irvine) adult dataset.

The original dataset already contains “training” and “test” datasets. Here we simply assume that the training and test data sets belong to different clients. So, we assign the training data and test data to two clients.

Now we use data utility to download UCI datasets to separate client package directory to /tmp/nvflare/data/ directory

Please note that the UCI website may experience occasional downtime.

python prepare_data.py

it should show something like

prepare data for data directory /tmp/nvflare/df_stats/data

download to /tmp/nvflare/df_stats/data/site-1/data.csv
skip empty line


download to /tmp/nvflare/df_stats/data/site-2/data.csv
skip empty line

done with prepare data

Client Code

Local statistics generator. The statistics generator AdultStatistics implements Statistics spec.

Client Code (client.py)
 1
 2from typing import Optional
 3
 4import pandas as pd
 5
 6from nvflare.apis.fl_context import FLContext
 7from nvflare.app_opt.statistics.df.df_core_statistics import DFStatisticsCore
 8
 9
10class AdultStatistics(DFStatisticsCore):
11    def __init__(self, filename, data_root_dir="/tmp/nvflare/df_stats/data"):
12        super().__init__()
13        self.data_root_dir = data_root_dir
14        self.filename = filename
15        self.data: Optional[dict[str, pd.DataFrame]] = None
16        self.data_features = [
17            "Age",
18            "Workclass",
19            "fnlwgt",
20            "Education",
21            "Education-Num",
22            "Marital Status",
23            "Occupation",
24            "Relationship",
25            "Race",
26            "Sex",
27            "Capital Gain",
28            "Capital Loss",
29            "Hours per week",
30            "Country",
31            "Target",
32        ]
33
34        # the original dataset has no header,
35        # we will use the adult.train dataset for site-1, the adult.test dataset for site-2
36        # the adult.test dataset has incorrect formatted row at 1st line, we will skip it.
37        self.skip_rows = {
38            "site-1": [],
39            "site-2": [0],
40        }
41
42    def load_data(self, fl_ctx: FLContext) -> dict[str, pd.DataFrame]:
43        client_name = fl_ctx.get_identity_name()
44        self.log_info(fl_ctx, f"load data for client {client_name}")
45        try:
46            skip_rows = self.skip_rows[client_name]
47            data_path = f"{self.data_root_dir}/{fl_ctx.get_identity_name()}/{self.filename}"
48            # example of load data from CSV
49            df: pd.DataFrame = pd.read_csv(
50                data_path, names=self.data_features, sep=r"\s*,\s*", skiprows=skip_rows, engine="python", na_values="?"
51            )
52            train = df.sample(frac=0.8, random_state=200)  # random state is a seed value
53            test = df.drop(train.index).sample(frac=1.0)
54
55            self.log_info(fl_ctx, f"load data done for client {client_name}")
56            return {"train": train, "test": test}
57
58        except Exception as e:
59            raise Exception(f"Load data for client {client_name} failed! {e}")
60
61    def initialize(self, fl_ctx: FLContext):
62        self.data = self.load_data(fl_ctx)

Many of the functions needed for tabular statistics have already been implemented DFStatisticsCore

In the AdultStatistics class, we really need to have the followings

  • data_features – here we hard-coded the feature name array.

  • implement load_data() -> Dict[str, pd.DataFrame] function, where the method will return a dictionary of panda DataFrames with one for each data source (“train”, “test”)

  • data_path = <data_root_dir>/<site-name>/<filename>

Server Code

The server aggregation have already implemented in Statistics Controller

Job Recipe

Job is defined via recipe, we will run it in Simulation Execution Env.

job Recipe (job.py)
 1import argparse
 2
 3from client import AdultStatistics
 4
 5from nvflare.recipe.fedstats import FedStatsRecipe
 6from nvflare.recipe.sim_env import SimEnv
 7
 8
 9def define_parser():
10    parser = argparse.ArgumentParser()
11    parser.add_argument("-n", "--n_clients", type=int, default=2)
12    parser.add_argument("-d", "--data_root_dir", type=str, nargs="?", default="/tmp/nvflare/df_stats/data")
13    parser.add_argument("-o", "--stats_output_path", type=str, nargs="?", default="statistics/adults_stats.json")
14
15    return parser.parse_args()
16
17
18def main():
19    args = define_parser()
20
21    n_clients = args.n_clients
22    data_root_dir = args.data_root_dir
23    output_path = args.stats_output_path
24
25    statistic_configs = {
26        "count": {},
27        "mean": {},
28        "sum": {},
29        "stddev": {},
30        "histogram": {"*": {"bins": 20}, "Age": {"bins": 20, "range": [0, 100]}},
31        "quantile": {"*": [0.1, 0.5, 0.9]},
32    }
33    # define local stats generator
34    df_stats_generator = AdultStatistics(filename="data.csv", data_root_dir=data_root_dir)
35
36    sites = [f"site-{i + 1}" for i in range(n_clients)]
37
38    recipe = FedStatsRecipe(
39        name="stats_df",
40        stats_output_path=output_path,
41        sites=sites,
42        statistic_configs=statistic_configs,
43        stats_generator=df_stats_generator,
44    )
45
46    env = SimEnv(clients=sites, num_threads=n_clients)
47    recipe.execute(env=env)
48
49
50if __name__ == "__main__":
51    main()

The statistics configuration determines which statistics we need generate Here is an example

statistic_configs = {
    "count": {},
    "mean": {},
    "sum": {},
    "stddev": {},
    "histogram": {"*": {"bins": 20}, "Age": {"bins": 20, "range": [0, 100]}},
    "quantile": {"*": [0.1, 0.5, 0.9]},
}

Run Job

from terminal try to run the code

python job.py

You should see something like

2025-09-03 20:42:03,392 - INFO - save statistics result to persistence store
2025-09-03 20:42:03,392 - INFO - job dir = /tmp/nvflare/simulation/stats_df/server/simulate_job
2025-09-03 20:42:03,395 - INFO - trying to save data to /tmp/nvflare/simulation/stats_df/server/simulate_job/statistics/adults_stats.json
2025-09-03 20:42:03,395 - INFO - file /tmp/nvflare/simulation/stats_df/server/simulate_job/statistics/adults_stats.json saved

The results are stored in workspace “/tmp/nvflare”

/tmp/nvflare/simulation/stats_df/server/simulate_job/statistics/adults_stats.json

Visualization

With JSON format, the data can be easily visualized via Pandas DataFrame and plots. Download and copy the output adults_stats.json file to the demo directory, then run the Jupyter notebook visualization.ipynb.