If you want the size to scale correctly, and print down to the pixel limit, the dpi and figure size need to be adjusted. Then s will behave correctly, starting with the smallest pixel being s=1. You also probably want to change the marker from the default. Try this:
#divide dpi by four, multiply figsize components by four to keep same area
fig, ax = plt.subplots(1,2, figsize=(24,12), dpi=72)
ax[0].scatter(X,Y, s=1, marker=".", c='k')
ax[1].scatter(X,Y, s=100, marker=".", c='k')
This will make the axes labels smaller, however.
Edit: We can also ensure that the s-value is never sub-minimal-pixel size, as pointed out in comments. Here's a function that allows one to play around with these settings using this:
def scat_rand(size=(24,12), scale=72, smin=1, smax=100, sedge=True, min_pix=1):
X = np.random.normal(0,1,5000)
Y = np.random.normal(0,1,5000)
fig, ax = plt.subplots(1,2, figsize=(size), dpi=scale)
slimit = (min_pix * 72 / fig.dpi) ** 2
if smin < slimit + .0001:
smin = slimit
print(f"smin too small, reset to {slimit}")
if sedge:
ax[0].scatter(X,Y, s=smin, c='k')
ax[1].scatter(X,Y, s=smax, c='k')
else:
ax[0].scatter(X,Y, s=smin, c='k', edgecolor='none')
ax[1].scatter(X,Y, s=smax, c='k', edgecolor='none')
plt.suptitle(f"Pixel= {size[0] * scale} x {size[1] * scale}", fontsize=14)
plt.show()
Here's a little driver if you want to play around with it:
if __name__ == "__main__":
temp = int(input("Enter size(1-24): "))
size = (2 * temp, temp)
scale = int(input("Enter dpi scale (72, 144, 288, 576, 1152): "))
limit = (72 / scale) ** 2
smin = float(input(f"Enter smallest point (limit = {limit}): "))
smax = float(input("Enter largest point: "))
edge = input("edgecolor (y/n): ")
if edge == 'y':
sedge = True
else:
sedge = False
scat_rand(size, scale, smin, smax, sedge)
See also:
Relationship between dpi and figure size