Skip to content

Add ClavaDDPM to SDGym multi table synthesizers - #630

Open
sarahmish wants to merge 21 commits into
mainfrom
clavaddpm
Open

Add ClavaDDPM to SDGym multi table synthesizers#630
sarahmish wants to merge 21 commits into
mainfrom
clavaddpm

Conversation

@sarahmish

@sarahmish sarahmish commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

original implementation is https://github.com/weipang142857/ClavaDDPM

The important class to review is ClavaDDPM and ClavaDDPMSynthesizer as these are the wrappers I wrote. I've written other code pieces, if it's taken from the source code directly, I mention "from" in the docstrings.

@sarahmish
sarahmish requested a review from a team as a code owner July 22, 2026 22:12
@sarahmish
sarahmish requested review from R-Palazzo and amontanez24 and removed request for a team July 22, 2026 22:12
@sarahmish sarahmish self-assigned this Jul 22, 2026
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.47401% with 95 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.03%. Comparing base (410fef0) to head (d1340b2).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
sdgym/synthesizers/clavaddpm.py 85.33% 95 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #630      +/-   ##
==========================================
+ Coverage   86.86%   87.03%   +0.17%     
==========================================
  Files          41       42       +1     
  Lines        4409     5061     +652     
==========================================
+ Hits         3830     4405     +575     
- Misses        579      656      +77     
Flag Coverage Δ
integration 56.11% <83.33%> (+4.42%) ⬆️
unit 76.64% <32.72%> (-6.51%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sarahmish
sarahmish requested a review from frances-h July 27, 2026 17:18
@sarahmish
sarahmish requested a review from pvk-developer July 29, 2026 18:21

@pvk-developer pvk-developer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When fitting I also see the steps (they come from TabDDPM), if verbose is set to False do we need to display them?

Comment thread sdgym/synthesizers/clavaddpm.py Outdated
Comment thread sdgym/synthesizers/clavaddpm.py Outdated
Comment thread sdgym/synthesizers/clavaddpm.py Outdated
Comment thread sdgym/synthesizers/clavaddpm.py Outdated
Comment thread sdgym/synthesizers/clavaddpm.py Outdated
@pvk-developer

pvk-developer commented Jul 31, 2026

Copy link
Copy Markdown
Member

During fit there is a point where MLoss and GLoss start to produce nan values, should we stop there for that table?

Step 9000/25000 MLoss: 2.7555 GLoss: 0.9242 Sum: 3.6797
Step 9500/25000 MLoss: nan GLoss: nan Sum: nan
...
Step 24500/25000 MLoss: nan GLoss: nan Sum: nan
Step 25000/25000 MLoss: nan GLoss: nan Sum: nan

@sarahmish
sarahmish removed the request for review from frances-h August 3, 2026 11:31
@sarahmish

Copy link
Copy Markdown
Contributor Author

During fit there is a point where MLoss and GLoss start to produce nan values, should we stop there for that table?

I lean towards keeping it as faithful to the original implementation as possible. Sometimes nan indicates that this model doesn't work for a particular dataset on this synthesizer.

@sarahmish
sarahmish requested a review from pvk-developer August 3, 2026 13:01
Base automatically changed from tabddpm to main August 3, 2026 13:55
Comment thread sdgym/synthesizers/clavaddpm.py
Comment thread sdgym/synthesizers/clavaddpm.py Outdated
@sarahmish
sarahmish requested a review from amontanez24 August 5, 2026 13:07
Comment thread sdgym/synthesizers/clavaddpm.py

LOGGER = logging.getLogger(__name__)
_MODEL_KWARGS = None
_MODALITY_FLAG = 'multi_table'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could remove this line since this is already set in the MultiTableBaselineSynthesizer:

_MODALITY_FLAG = 'multi_table'

@R-Palazzo R-Palazzo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good!

I’m still reviewing the logic and checking that it aligns with their implementation. In the meantime, I’m thinking about two things:

  1. For the structure, we could either:
  • Define a clavaddpm folder where we keep files such as pipeline_modules.py and preprocess_utils.py, along with our SDGym logic.
  • Keep everything in one file, but be more explicit about what comes from their implementation versus what we added. It could look something like:
######################### SDGym extra logic #########################

def get_group_data_dict(np_data, group_id_attrs=[0]):
    """Grouping dictionary from pipeline_utils.py."""
    group_data_dict = {}
    data_len = len(np_data)
    for i in range(data_len):
        row_id = tuple(np_data[i, group_id_attrs])
        if row_id not in group_data_dict:
            group_data_dict[row_id] = []
        group_data_dict[row_id].append(np_data[i])
    return group_data_dict


######################## From preprocess_utils.py ###################

def get_domain(df, id_cols, discrete_cols):
    """Build the ``{col: {'size', 'type'}}`` domain of a table from preprocess_utils.py."""
    domain = {}
    for col in df.columns:
        if col in discrete_cols:
            domain[col] = {'size': len(df[col].unique()), 'type': 'discrete'}
        elif col not in id_cols:
            domain[col] = {'size': len(df[col].unique()), 'type': 'continuous'}

    return domain
.
.
.
######################## From pipeline_modules.py ###################
.
.
.
  1. Would it be possible to have an integration test that runs the SDGym ClavaDDPM implementation against their original implementation and checks that the results are the same or similar? It would be nice to have, although I’m not sure how feasible it is. It might require adding their repo as a test dependency.



def match_tables(A, B, n_clusters=25, unique_matching=True, batch_size=100):
"""Nearest-neighbour match of every row of ``A`` to a row of ``B``.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add from pipeline_utils.py

batch_size=100,
no_matching=False,
):
"""Reconcile a child generated once per parent into a single table."""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also add from pipeline_utils.py

meta = metadata if isinstance(metadata, dict) else metadata.to_dict()

self._table_names = list(meta['tables'])
self._primary_key = {}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also define in l804

self.matching_batch_size = matching_batch_size
self.unique_matching = unique_matching
self.no_matching = no_matching
self.max_categories = max_categories

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to never be used, should we get rid of it?

chosen, chosen_distance = int(candidate), float(distance)
break
if chosen is None:
chosen = next(j for j in range(len(B)) if j not in used)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also update chosen_distance here?


Mirrors ``GaussianMultinomialDiffusion._sample`` but takes explicit
per-row labels instead of drawing them from the empirical distribution.
Similar to the original classifier-guided ``conditional_sample`` implemtation.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Similar to the original classifier-guided ``conditional_sample`` implemtation.
Similar to the original classifier-guided ``conditional_sample`` implementation.

Comment on lines +213 to +214
encoder's fitted range get fresh values: numeric ids continue after the
largest original id, other ids become new ``'{column}_{code}'`` strings.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to handle the numerical case here?

In l875 and l883 we're skipping the encoder for numerical ids

parent_num_cols.append((col_index, col))

parent_primary_key_index = original_parent_cols.index(parent_primary_key)
foreing_key_index = original_child_cols.index(foreign_key)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreing_key_index = original_child_cols.index(foreign_key)
foreign_key_index = original_child_cols.index(foreign_key)

parent_scale * cat_one_hot[:, joint_cat_matrix_p_index:]
)

# Perform quantile normalization using QuantileTransformer

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We removed num_quantile because it was not used?
https://github.com/weipang142857/ClavaDDPM/blob/fa6ef4d7c6d2c584b45a398c4471bb51e0bb5e17/pipeline_modules.py#L243

Maybe we should update the comment

num_clusters = min(num_clusters, len(cluster_data))

init_param = 'k-means++'
if SKLEARN_VERSION.release[:2] < (1, 1):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n_init='auto' is also impacted by the sklearn version. It was introduced in scikit-learn 1.2:

https://scikit-learn.org/stable/whats_new/v1.2.html

At some point maybe we could just bump the minimum version for sklearn

@R-Palazzo
R-Palazzo self-requested a review August 17, 2026 12:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants