Repository: wandb/wandb

Issue:
[Feature]: Line plots smart sampling
### Description

In the line plot documentation it says "When over 1500 points are chosen for a line plot metric, W&B returns 1500 randomly sampled points." This is fine in most cases, but it becomes annoying in the case of showing x-axis with log scale. This makes the visualization very distorted for the first few data points and would dominate the whole plot when the datapoints keep increasing. A use case is attached.
![W B Chart 7_9_2023, 12_16_04 AM](https://github.com/wandb/wandb/assets/72370449/439e783a-1920-4998-b527-b35f466cd7f0)


### Suggested Solution

In the case of log scale, sample datapoints according to an exponentially decaying weight such that it won't distort the visualization.

### Alternatives

_No response_

### Additional Context

_No response_


Discussion:
Hi @ryanliu30, thank you for the feature request and example. I see how favoring points from the earlier steps would make more sense here. I can submit this to the engineering team and follow up once they have a chance to look into this. 

Resolving patch:
diff --git a/wandb/plot/line_series.py b/wandb/plot/line_series.py
--- a/wandb/plot/line_series.py
+++ b/wandb/plot/line_series.py
@@ -69,7 +69,6 @@ def line_series(
 
     if keys is not None:
         assert len(keys) == len(ys), "Number of keys and y-lines must match"
-
     data = [
         [x, f"key_{i}" if keys is None else keys[i], y]
         for i, (xx, yy) in enumerate(zip(xs, ys))
diff --git a/wandb/util.py b/wandb/util.py
--- a/wandb/util.py
+++ b/wandb/util.py
@@ -1813,3 +1813,26 @@ def random_string(length: int = 12) -> str:
     return "".join(
         secrets.choice(string.ascii_lowercase + string.digits) for _ in range(length)
     )
+
+
+def sample_with_exponential_decay_weights(
+    xs: Union[Iterable, Iterable[Iterable]],
+    ys: Iterable[Iterable],
+    keys: Optional[Iterable] = None,
+    sample_size: int = 1500,
+) -> Tuple[List, List, Optional[List]]:
+    """Sample from a list of lists with weights that decay exponentially.
+
+    May be used with the wandb.plot.line_series function.
+    """
+    xs_array = np.array(xs)
+    ys_array = np.array(ys)
+    keys_array = np.array(keys) if keys else None
+    weights = np.exp(-np.arange(len(xs_array)) / len(xs_array))
+    weights /= np.sum(weights)
+    sampled_indices = np.random.choice(len(xs_array), size=sample_size, p=weights)
+    sampled_xs = xs_array[sampled_indices].tolist()
+    sampled_ys = ys_array[sampled_indices].tolist()
+    sampled_keys = keys_array[sampled_indices].tolist() if keys else None
+
+    return sampled_xs, sampled_ys, sampled_keys


Test patch:
diff --git a/tests/pytest_tests/unit_tests/test_util.py b/tests/pytest_tests/unit_tests/test_util.py
--- a/tests/pytest_tests/unit_tests/test_util.py
+++ b/tests/pytest_tests/unit_tests/test_util.py
@@ -760,3 +760,14 @@ def test_make_docker_image_name_safe():
         == "abc.123__def-456"
     )
     assert util.make_docker_image_name_safe("......") == "image"
+
+
+def test_sampling_weights():
+    xs = np.arange(0, 100)
+    ys = np.arange(100, 200)
+    sample_size = 1000
+    sampled_xs, _, _ = util.sample_with_exponential_decay_weights(
+        xs, ys, sample_size=sample_size
+    )
+    # Expect more samples from the start of the list
+    assert np.mean(sampled_xs) < np.mean(xs)