If my_array has a typical length of about 10 or more, it can be worthwhile to convert your array to the integers [0, 1, 2] and then apply bincount().
Here's an example with your my_array:
In [31]: my_array = np.array(["a", "a", "c", "c", "a"])
In [32]: b = my_array.view(np.int32) - ord('a')
In [33]: b
Out[33]: array([0, 0, 2, 2, 0], dtype=int32)
In [34]: np.bincount(b, minlength=3)
Out[34]: array([3, 0, 2])
Here's a timing comparison of that method and collections.Counter using an input with length 100:
In [34]: rng = np.random.default_rng()
In [35]: a = rng.choice(['a', 'a', 'b', 'c'], size=100)
In [36]: %timeit Counter(a)
32.1 µs ± 723 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In [37]: %timeit b = a.view(np.int32) - ord('a'); np.bincount(b, minlength=3)
3.86 µs ± 50.7 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
The approach with bincount() is much faster.
It is also faster than using np.unique() with the parameter return_counts=True:
In [41]: %timeit values, counts = np.unique(a, return_counts=True)
19.7 µs ± 274 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)