Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dp_wizard/shiny/components/icons.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from faicons import icon_svg

# Find more icons on Font Awesome: https://fontawesome.com/search?ic=free

data_source_icon = icon_svg("file")
unit_of_privacy_icon = icon_svg("shield-halved")
product_icon = icon_svg("cart-shopping")
Expand Down
22 changes: 10 additions & 12 deletions dp_wizard/shiny/panels/analysis_panel/column_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
only_for_screenreader,
tutorial_box,
)
from dp_wizard.types import AnalysisName, ColumnName, Product
from dp_wizard.types import AnalysisName, ColumnName, Product, Weight
from dp_wizard.utils.code_generators import make_column_config_block
from dp_wizard.utils.code_generators.analyses import (
get_analysis_by_name,
Expand All @@ -29,7 +29,6 @@
from dp_wizard.utils.shared import plot_bars

default_analysis_type = histogram.name
default_weight = "2"
label_width = "10em" # Just wide enough so the text isn't trucated.


Expand Down Expand Up @@ -141,7 +140,7 @@ def column_server(
lower_bounds: reactive.Value[dict[ColumnName, float]],
upper_bounds: reactive.Value[dict[ColumnName, float]],
bin_counts: reactive.Value[dict[ColumnName, int]],
weights: reactive.Value[dict[ColumnName, str]],
weights: reactive.Value[dict[ColumnName, Weight]],
is_tutorial_mode: reactive.Value[bool],
is_sample_csv: bool,
is_single_column: bool,
Expand All @@ -150,7 +149,10 @@ def column_server(
def _set_hidden_inputs():
# TODO: Is isolate still needed?
with reactive.isolate(): # Without isolate, there is an infinite loop.
ui.update_numeric("weight", value=int(weights().get(name, default_weight)))
ui.update_numeric(
"weight",
value=int(weights().get(name, Weight.DEFAULT).value),
)

@reactive.effect
@reactive.event(input.analysis_type)
Expand Down Expand Up @@ -187,15 +189,15 @@ def _set_bins():
@reactive.effect
@reactive.event(input.weight)
def _set_weight():
weights.set({**weights(), name: input.weight()})
weights.set({**weights(), name: Weight(input.weight())})

@reactive.calc()
def accuracy_histogram():
lower_x = float(input.lower_bound())
upper_x = float(input.upper_bound())
bin_count = int(input.bins())
weight = float(input.weight())
weights_sum = sum(float(weight) for weight in weights().values())
weights_sum = sum(float(weight.value) for weight in weights().values())
info(f"Weight ratio for {name}: {weight}/{weights_sum}")
if weights_sum == 0:
# This function is triggered when column is removed;
Expand Down Expand Up @@ -349,12 +351,8 @@ def optional_weight_ui():
ui.input_select(
"weight",
"Weight",
choices={
"1": "Less accurate",
default_weight: "Default",
"4": "More accurate",
},
selected=default_weight,
choices={w.value: str(w) for w in Weight},
selected=Weight.DEFAULT.value,
width=label_width,
),
tutorial_box(
Expand Down
2 changes: 1 addition & 1 deletion dp_wizard/shiny/panels/results_panel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ def analysis_plan() -> AnalysisPlan:
lower_bound=lower_bounds()[col],
upper_bound=upper_bounds()[col],
bin_count=int(bin_counts()[col]),
weight=int(weights()[col]),
weight=int(weights()[col].value),
)
]
for col in weights().keys()
Expand Down
18 changes: 17 additions & 1 deletion dp_wizard/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@
from shiny import reactive


class Weight(Enum):
"""
>>> print(Weight.MORE_ACCURATE)
More accurate
>>> ints = [int(w.value) for w in Weight]
>>> assert ints[2] / ints[1] == ints[1] / ints[0]
"""

MORE_PRIVATE = "1"
DEFAULT = "2"
MORE_ACCURATE = "4"

def __str__(self) -> str:
return self.name.replace("_", " ").capitalize()


class Product(Enum):
STATISTICS = auto()
SYNTHETIC_DATA = auto()
Expand Down Expand Up @@ -109,7 +125,7 @@ class AppState:
lower_bounds: reactive.Value[dict[ColumnName, float]]
upper_bounds: reactive.Value[dict[ColumnName, float]]
bin_counts: reactive.Value[dict[ColumnName, int]]
weights: reactive.Value[dict[ColumnName, str]]
weights: reactive.Value[dict[ColumnName, Weight]]
analysis_errors: reactive.Value[dict[ColumnName, bool]]

# Release state:
Expand Down
23 changes: 13 additions & 10 deletions tests/utils/test_code_generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def test_make_column_config_block_for_histogram():
)


abc_csv = "tests/fixtures/abc.csv"
abc_csv_path = str((package_root.parent / "tests/fixtures/abc.csv").absolute())


def number_lines(text: str):
Expand Down Expand Up @@ -141,22 +141,17 @@ def number_lines(text: str):


def id_for_plan(plan: AnalysisPlan):
columns = ", ".join(f"{v[0].analysis_name} of {k}" for k, v in plan.columns.items())
description = (
f"{plan.product} for {columns}; "
f"grouped by ({', '.join(plan.groups) or 'nothing'})"
)
return re.sub(r"\W+", "_", description) # For selection with "pytest -k substring"
return re.sub(r"\W+", "_", str(plan)) # For selection with "pytest -k substring"


plans = [
plans_all_combos = [
AnalysisPlan(
product=product,
groups=groups,
columns=columns,
contributions=contributions,
contributions_entity="Family",
csv_path=abc_csv,
csv_path=abc_csv_path,
epsilon=1,
max_rows=100_000,
)
Expand All @@ -178,6 +173,13 @@ def id_for_plan(plan: AnalysisPlan):
]


# The matrix is very redundant! A subsample is sufficient,
# but make sure it's relatively prime so we have coverage.
mod = 7
assert len(plans_all_combos) % mod != 0
plans = [plan for i, plan in enumerate(plans_all_combos) if i % 7 == 0]


expected_urls = [
"https://docs.opendp.org/",
"https://github.com/opendp/dp-wizard",
Expand Down Expand Up @@ -230,6 +232,7 @@ def test_make_notebook(plan):
@pytest.mark.parametrize("plan", plans, ids=id_for_plan)
def test_make_script(plan):
script = ScriptGenerator(plan, "Note goes here!").make_py()
print(number_lines(script))

# Make sure jupytext formatting doesn't bleed into the script.
# https://jupytext.readthedocs.io/en/latest/formats-scripts.html#the-light-format
Expand All @@ -241,6 +244,6 @@ def test_make_script(plan):
fp.flush()

result = subprocess.run(
["python", fp.name, "--csv", abc_csv], capture_output=True
["python", fp.name, "--csv", abc_csv_path], capture_output=True
)
assert result.returncode == 0
Loading