Parameter Estimation of the Ideal Distribution of Visual Meteor Magnitudes

2026-09-12

Introduction

The density of the ideal distribution of meteor magnitudes is \[ {\displaystyle f(m) = \frac{\mathrm{d}p}{\mathrm{d}m} = \frac{3}{2} \, \log(r) \sqrt{\frac{r^{3 \, \psi + 2 \, m}}{(r^\psi + r^m)^5}}} \] where \(m\) denotes the continuous (real-valued) meteor magnitude, \(r = 10^{0.4} \approx 2.51189 \dots\) is a constant, and \(\psi\) is the only parameter of this magnitude distribution.

In visual meteor observations, magnitudes are usually estimated as integer values. Hence, this distribution is discrete and its probability mass function is given by \[ P[M = m] \sim \begin{cases} g(m_{\mathrm{lim}} - m) \displaystyle \int\limits_{m-0.5}^{m+0.5} f(u) \, \mathrm{d}u, & \text{if } m_{\mathrm{lim}} - m > -0.5,\\[5pt] 0 & \text{otherwise,} \end{cases} \] where \(m_{\mathrm{lim}}\) denotes the limiting (non-integer) magnitude of the observation, and \(m\) the integer meteor magnitude. The function \(f(\cdot)\) is the continuous density of the ideal magnitude distribution, and \(g(\cdot)\) denotes the perception probability function.

Here we demonstrate two methods to estimate the parameter \(\psi\).

First, we obtain some magnitude observations from the example data set, which also includes the limiting magnitude.

observations <- with(PER_2015_magn$observations, {
    idx <- !is.na(lim_magn) & sl_start > 135.81 & sl_end < 135.87
    data.frame(
        magn_id = magn_id[idx],
        lim_magn = lim_magn[idx]
    )
})
head(observations, 5) # Example values
magn_id lim_magn
225413 5.30
225432 5.95
225438 6.01
225449 6.48
225496 5.50

Next, the observed meteor magnitudes are matched with the corresponding observations. This is necessary as we need the limiting magnitudes of the observations to determine the parameter.

Using

magnitudes <- with(new.env(), {
    magnitudes <- merge(
        observations,
        as.data.frame(PER_2015_magn$magnitudes),
        by = "magn_id"
    )
    magnitudes$magn <- as.integer(as.character(magnitudes$magn))
    subset(magnitudes, (magnitudes$lim_magn - magnitudes$magn) > -0.5)
})
head(magnitudes[magnitudes$Freq > 0, ], 5) # Example values

we obtain a data frame with the absolute observed frequencies Freq for each observation of a magnitude class. The expression subset(magnitudes, (magnitudes$lim_magn - magnitudes$magn) > -0.5 ensures that meteors fainter than the limiting magnitude are not used if they exist.

magn_id lim_magn magn Freq
9 225413 5.30 4 1.0
11 225413 5.30 1 2.0
14 225413 5.30 3 3.0
15 225432 5.95 4 2.0
17 225432 5.95 3 1.5

This data frame contains a total of 97 meteors. This is a sufficiently large number to estimate the parameter.

Maximum Likelihood Method

The maximum likelihood method can be used to estimate the parameter in an asymptotically unbiased manner. For this, the function dvmideal() is needed, which returns the probability density of the observable meteor magnitudes when the parameter and the limiting magnitudes are known.

The following algorithm estimates the parameter by maximizing the likelihood with the optim() function. The function ll() returns the negative log-likelihood, as optim() identifies a minimum.

# maximum likelihood estimation (MLE) of psi
result_ml <- with(magnitudes, {
    # log likelihood function
    ll <- function(psi) -sum(Freq * dvmideal(magn, lim_magn, psi, log = TRUE))
    psi_start <- 6.0 # starting value
    psi_lower <- 4.0 # lowest expected value
    psi_upper <- 10.0 # highest expected value
    # find minimum
    optim(psi_start, ll, method = "Brent", lower = psi_lower, upper = psi_upper, hessian = TRUE)
})

This gives the expected value and the variance of the parameter:

psi_mean <- result_ml$par # mean of psi
print(psi_mean)
#> [1] 6.116217
psi_var <- 1 / result_ml$hessian[1][1] # variance of psi
print(psi_var)
#> [1] 0.3335565

optim() requires both bounds to be finite, so psi_upper cannot be set to infinity. That bound is an assumption, and it is worth testing rather than trusting: an estimate that has merely run into it would be reported like any other. Since dvmideal() accepts an infinite \(\psi\), the likelihood can be evaluated at the limit itself and compared with the estimate.

ll_at <- function(psi) with(magnitudes, sum(Freq * dvmideal(magn, lim_magn, psi, log = TRUE)))
print(c("psi_hat" = ll_at(psi_mean), "Inf" = ll_at(Inf)))
#>   psi_hat       Inf 
#> -173.0567 -176.3362

The estimate is the better of the two, so the maximum lies within the interval and \(\psi\) is identified. Had the limit won instead, the maximum likelihood estimate would be Inf: the magnitudes are then geometric with \(r = 10^{0.4}\), and no finite \(\psi\) describes them better. Raising psi_upper would not help in that case, and beyond about 35 it becomes actively harmful here — the likelihood is flat above \(\psi \approx 16.5\), and once that plateau dominates the search interval, optim() settles on it instead of finding the maximum.

We can additionally visualize the likelihood function here.

with(new.env(), {
    data_plot <- data.frame(psi = seq(4.0, 11, 0.1))
    data_plot$ll <- mapply(function(psi) {
        with(magnitudes, {
            # log likelihood function
            sum(Freq * dvmideal(magn, lim_magn, psi, log = TRUE))
        })
    }, data_plot$psi)
    data_plot$l <- exp(data_plot$ll - max(data_plot$ll))
    data_plot$l <- data_plot$l / sum(data_plot$l)
    plot(data_plot$psi, data_plot$l,
        type = "l",
        col = "blue",
        xlab = "psi",
        ylab = "likelihood"
    )
    abline(v = result_ml$par, col = "red", lwd = 1)
})

It is clearly visible that the likelihood function is not normally distributed. This distribution even belongs to the class of heavy-tailed distributions. While its maximum is indeed an asymptotically unbiased estimator, this does not hold for the variance. This is important in this context because the variance of the estimated \(\psi\)-value is derived from the curvature (the second derivative at the maximum) of the log-likelihood function. Therefore, the estimator for the variance of \(\psi\) is far too small.

Variance-Stabilizing Transformation

Estimation based on the maximum likelihood principle is computationally demanding. As an alternative to the method of the previous section, a variance-stabilizing transformation can be applied. It maps meteor magnitudes onto a different scale, yielding a distribution whose variance no longer depends on the parameter \(\psi\), so that a plain mean of the transformed magnitudes estimates \(\psi\).

What this buys is a fixed variance of 1.0, and several things follow from it:

The mean of the transformed magnitudes is therefore a well-conditioned estimator, and its dispersion no longer depends on the parameter being estimated. It is worth being precise about what this does and does not mean: the sample mean becomes easy to work with, but the transformed magnitudes do not thereby become a quantity that is linear in covariates. The next paragraphs return to this point, because the distinction decides which tool applies.

The resulting procedure is straightforward: it suffices to compute the mean of the transformed meteor magnitudes, from which an estimate of the parameter \(\psi\) is obtained.

vmideal_vst_from_magn() performs the mapping. It is monotonically decreasing: a bright meteor is far from the limiting magnitude and receives a large tm, a faint one a small value. Shown here for a limiting magnitude of 6.0.

with(new.env(), {
    lim_magn <- 6.0
    m <- seq(-4, 6, 0.05)
    plot(m, vmideal_vst_from_magn(m, lim_magn),
        type = "l",
        col = "blue",
        xlab = "m",
        ylab = "tm"
    )
    abline(h = 0.0, lwd = 1)
})

Note that tm becomes negative for meteors close to the limiting magnitude. This is where the transformation runs out: vmideal_vst_to_psi() maps the mean back onto \(\psi\), and it is defined from tm = 0.02 upwards, reporting Inf below that value. The two directions therefore cover different ranges of tm, which is not a defect but the statement that faint meteors no longer resolve a finite \(\psi\).

with(new.env(), {
    lim_magn <- 6.0
    tm <- seq(0.02, 8.22, 0.02)
    plot(tm, vmideal_vst_to_psi(tm, lim_magn),
        type = "l",
        col = "blue",
        xlab = "tm",
        ylab = "psi"
    )
    abline(h = lim_magn, col = "red", lwd = 1)
})

The red line marks the limiting magnitude itself. Note that \(\psi\) decreases as tm grows: the region above the line, where \(\psi\) exceeds the limiting magnitude and the mean of the distribution begins to flatten, is therefore the narrow band at small tm on the left. That is where the estimate loses its resolution, and it is also why a larger tm corresponds to a smaller \(\psi\) — a relation the confidence bound below relies on.

Note that the variance-stabilizing transformation yields a mean that can be directly analyzed, but converting it back to the \(\psi\) parameter requires applying the delta method. This accounts for the nonlinearity of the transformation and provides appropriate uncertainty estimates for \(\psi\).

Because the back-transformation returns \(\psi\) relative to a limiting magnitude, a reference point has to be fixed before anything can be estimated. The transformed magnitude tm measures how far \(\psi\) lies behind the limiting magnitude of the observation, so a value of tm on its own does not identify \(\psi\) — only together with the limiting magnitude it refers to. The mean limiting magnitude of the data set is the natural choice, being the point at which the estimate is best supported.

lim_magn_mean <- with(magnitudes, {
    N <- sum(Freq)
    sum(Freq * lim_magn) / N
})
print(paste("lim_magn_mean:", lim_magn_mean))
#> [1] "lim_magn_mean: 5.65752577319588"
tm_mean <- with(magnitudes, {
    N <- sum(Freq)
    tm <- vmideal_vst_from_magn(magn, lim_magn)
    tm_mean <- sum(Freq * tm) / N
    tm_var <- sum(Freq * (tm - tm_mean)^2) / (N - 1)
    tm_mean_var <- tm_var / N
    list("val" = tm_mean, "var" = tm_mean_var, "sd" = sqrt(tm_mean_var))
})

Thus, one obtains the mean and the variance of the mean of tm.

print(paste("tm mean:", tm_mean$val))
#> [1] "tm mean: 0.0878019525445456"
print(paste("tm var:", tm_mean$var))
#> [1] "tm var: 0.00836015180816983"

Using the bootstrap method, it can be assessed whether the mean is normally distributed.

tm_means <- with(magnitudes, {
    N <- sum(Freq)
    tm <- vmideal_vst_from_magn(magn, lim_magn)
    replicate(50000, {
        mean(sample(tm, size = N, replace = TRUE, prob = Freq))
    })
})

The graphical representation indicates that this is indeed approximately the case.

with(new.env(), {
    tm_min <- tm_mean$val - 3 * tm_mean$sd
    tm_max <- tm_mean$val + 3 * tm_mean$sd
    tm_means <- subset(tm_means, tm_means > tm_min & tm_means < tm_max)
    brks <- seq(min(tm_means) - 0.02, max(tm_means) + 0.02, by = 0.02)
    hist(tm_means,
        breaks = brks,
        col = "skyblue",
        border = "black",
        main = "Histogram of mean tm",
        xlab = "tm",
        ylab = "count",
        xaxt = "n"
    )
    axis(1, at = seq(round(min(brks), 1), round(max(brks), 1) + 0.1, by = 0.1))
    abline(v = 0, col = "red", lwd = 1)
})

A mean value approaching 0.0 implies that \(\psi\) lies at infinity, and vmideal_vst_to_psi() reports Inf once it has fallen below 0.02, where no finite \(\psi\) is resolved any more. Negative values can be interpreted as a kind of “beyond infinity”, which is not meaningful. There are two possible explanations:

  1. The distribution is not ideal, i.e., the observations do not fit the model as described above.
  2. Random variation led to this result.

This data set is a case in point. The mean lies only about one standard deviation above 0.0, and roughly a quarter of the bootstrap replicates fall below the threshold at which no finite \(\psi\) is resolved. The point estimate is therefore better read as a median than as an expected value, and the confidence bound given below is the more honest statement.

print(paste("mean psi:", vmideal_vst_to_psi(tm_mean$val, lim_magn_mean)))
#> [1] "mean psi: 7.00407818720334"

No second-order term of the delta method is applied here, and this is the same circumstance seen from another side. vmideal_vst_to_psi() supplies the necessary derivatives through deriv_degree, as vignette("vmgeom") uses them, but such a correction presupposes that the back-transformation is nearly straight over the range the estimate covers. Here it is not: the second-order term amounts to some nine percent of \(\psi\), and the lower end of the interval falls below the threshold where \(\psi\) ceases to be finite. A correction of that size no longer corrects anything — it reports that the expansion it comes from does not apply. Where the estimate is well separated from the threshold the term is legitimate, and then it is too small to matter.

This is the case the transformation is made for: a single, global \(\psi\) that does not vary across the data set. What the observations resolve is the difference between \(\psi\) and the limiting magnitude of each observation, and averaging the transformed magnitudes before converting them back estimates that difference at the reference point.

One qualification belongs here. When the limiting magnitudes differ, the mean of the transformed magnitudes is not exactly the value that belongs to their mean, because the back-transformation is curved rather than straight. The resulting shift is small, and it shrinks the more similar the limiting magnitudes are. If all observations share the same limiting magnitude, no averaging over differing limiting magnitudes is involved at all, and the reference point is exact.

By contrast, if \(\psi\) is significantly larger than the limiting magnitude, estimation becomes problematic: \(\psi\) effectively tends to infinity, and the observable magnitude distribution approaches the geometric model of visual meteor magnitudes with a population index of \(r \approx 2.5\).

In practice, however, it is preferable to use a confidence interval estimate. For example, one can estimate that \(\psi\) is, with 10 percent probability, not smaller than:

print(vmideal_vst_to_psi(qnorm(0.90, tm_mean$val, tm_mean$sd), lim_magn_mean))
#> [1] 5.827908

How accurate the result is depends on two separate things. Over the range the transformation covers — up to about three magnitudes above the limiting magnitude — it reproduces \(\psi\) to within a few hundredths; this is the error of the back-transformation, and it stays well below the sampling error even for very large data sets, so it is not what limits an estimate. What does limit it is that sampling error, and here the transformation is somewhat less precise than the maximum likelihood estimate of the previous section.

What it offers in exchange is speed. The transformation is applied once per meteor and everything afterwards is an ordinary mean, whereas maximizing the likelihood iterates over the distribution. For a single estimate this hardly matters, as both are fast enough in absolute terms. It becomes decisive as soon as the estimate is repeated: the bootstrap above draws fifty thousand replicates and still runs in a fraction of a second. The same applies when \(\psi\) is estimated separately for many time intervals or showers.

It is tempting to go one step further. Since the transformed magnitudes are homoscedastic, they look like the ideal input for lm(), with the limiting magnitude and any covariate of interest entering as regressors. That step does not work, and it is worth saying why.

The expected value of tm depends on \(\psi\) and the limiting magnitude only through their difference, which is what makes a single reference point meaningful in the first place. That dependence, however, is not linear: it falls off roughly exponentially. A straight line on the tm scale is therefore a curve on the \(\psi\) scale, and a \(\psi\) profile that is in truth linear in a covariate comes back out of such a fit bent. The estimate is biased, not merely imprecise.

What makes this worth a warning is that the usual diagnostics do not reveal it. Precisely because the transformation makes the residuals homoscedastic, such a fit looks unremarkable: the residual variance is where it should be, and tests for curvature rarely respond. The discrepancy sits in the expected value rather than in the dispersion, so a clean residual plot is no evidence that the model holds here. Nor does a change of scale help — taking logarithms, for instance, improves the shape but not the conclusion, because the influence of the limiting magnitude then depends on the very \(\psi\) that is being estimated.

The geometric model behaves differently in this respect, which is instructive. Its transformation is built on the ratio of the perception probabilities of two adjacent magnitude classes, whose mean is \(1/r\) and, unlike the magnitudes themselves, no longer depends on the limiting magnitude at all. Accordingly vmgeom_vst_to_r() takes no limiting magnitude: there is no reference point to fix, and none to carry through a model. That is what makes the transformed magnitudes usable with lm() there, and it is precisely what the ideal distribution does not offer, since the limiting magnitude cannot be eliminated from \(\psi\) but only measured against it.

The difference also shows in the form of the two back-transformations. For the geometric model it is a power relation, hence an exact straight line on the logarithmic scale, so that effects combine additively there. Here it is a calibrated polynomial in \(\log (\texttt{tm})\) with no such form, and no change of scale produces one. Of the three properties by which transformations of this kind are classically judged — constant variance, symmetry and additivity — this one delivers the first and not the last.

Beyond estimating \(\psi\), the common scale is useful in its own right. Since transformed magnitudes carry the same variance regardless of the limiting magnitude they were observed at, subsets of the data can be compared with each other directly — the mean of tm per observer or per night, for instance — which is not possible on the raw magnitude scale, where any such comparison is confounded by the limiting magnitude.

For a \(\psi\) that varies with a covariate, neither this transformation nor the maximum likelihood estimate of the previous section is sufficient. That is the subject of the next section.

Generalized Linear Model

Both methods so far assume that \(\psi\) is a single constant. A generalized linear model lifts that restriction: it estimates \(\psi\) as a function of covariates, and the limiting magnitude is carried along per observation instead of being reduced to a common reference point. vmideal_glm() fits the ideal distribution through glm().

As for the geometric model, the response is passed as a two-column matrix holding the meteor magnitude and the corresponding limiting magnitude, so that the limiting magnitude is subsetted together with the remaining data whenever glm() drops rows. The observed frequencies enter as weights.

# rows with a frequency of zero carry no information
magnitudes_obs <- subset(magnitudes, magnitudes$Freq > 0)

result_glm <- vmideal_glm(
    cbind(magn, lim_magn) ~ 1,
    data = magnitudes_obs,
    weights = magnitudes_obs$Freq
)

The link is the identity, since \(\psi\) is unrestricted on the real line. predict() therefore returns \(\psi\) itself.

# the model has no covariates, so any single row predicts the global psi
newdata <- data.frame(row.names = "")
print(c(
    "ML" = result_ml$par,
    "GLM" = as.numeric(predict(result_glm, newdata))
))
#>       ML      GLM 
#> 6.116217 7.429923

Unlike vmgeom_glm(), the two estimates do not agree exactly, and this is a property of the model rather than of the implementation.

A glm() fit works entirely through the expected magnitude \(\mu = \mathrm{E}[M]\), which the model predicts for a given limiting magnitude and parameter. It compares each observed magnitude \(m\) with that expectation and adjusts the parameter by the difference \(m - \mu\). This reproduces the maximum likelihood estimate exactly whenever the deviation \(m - \mu\) carries all the information the magnitude has about the parameter — in other words, whenever the mean is a sufficient statistic.

For the geometric model that is the case: \(\log f\) is linear in the magnitude, so a meteor twice as far from the expectation contributes twice as much. The density of the ideal distribution contains the term \(r^\psi + r^m\), which makes \(\log f\) non-linear in \(m\). How much a faint meteor really tells us about \(\psi\) therefore levels off, while the difference \(m - \mu\) that glm() uses in its place keeps growing. The two stop being proportional, and the mean is not sufficient for \(\psi\).

vmideal_glm() is therefore a quasi-likelihood estimator. It is consistent, but not fully efficient, and how much it loses depends on how far \(\psi\) lies above the limiting magnitude. While \(\psi\) stays below it, the variance is within about ten percent of the Cramér-Rao bound; once \(\psi\) exceeds it, the factor grows to roughly \(1.4\) to \(2.2\), the larger values belonging to the smaller limiting magnitudes.

The difference of more than a magnitude between the two estimates above is a case in point. This excerpt contains only 97 meteors at limiting magnitudes between 4.93 and 6.48, and the estimate already sits above the mean limiting magnitude, where the mean of the distribution begins to flatten. The two estimators are then far less tightly coupled than they are for a few thousand meteors at a moderate \(\psi\), where they typically differ by a few hundredths.

The standard error reflects this honestly:

print(predict(result_glm, newdata, se.fit = TRUE))
#> $fit
#>          
#> 7.429923 
#> 
#> $se.fit
#> [1] 1.626747
#> 
#> $lwr
#>          
#> 4.241559 
#> 
#> $upr
#>          
#> 10.61829

That is also the limit worth knowing. What the data can resolve is governed by the distance between \(\psi\) and the limiting magnitude, not by \(\psi\) itself. Up to about two to three magnitudes above the limiting magnitude \(\psi\) is estimated reliably; beyond that the mean of the distribution flattens, and once it has reached the value it converges to, no data can tell a larger \(\psi\) from a smaller one.

This never fails silently. The standard error grows as the mean flattens, and where \(\psi\) is no longer identified at all, predict() returns Inf:

# magnitudes fainter than the ideal distribution can produce
faint <- data.frame(magn = c(rep(3L, 40), rep(4L, 40), rep(5L, 20)), lim_magn = 5.5)
result_faint <- vmideal_glm(cbind(magn, lim_magn) ~ 1, data = faint)
psi_faint <- predict(result_faint, faint[1, ])
print(psi_faint)
#>   1 
#> Inf

Inf is a result rather than an error: it says the magnitudes are as faint as the model can make them, so \(\psi\) is bounded below by the fit rather than determined by it. Such data are either a small-sample accident or a sign that the ideal distribution is not the right model for them. The linear predictor stays finite in both cases, so summary() and anova() remain usable.

Being a result, it can be used as one. dvmideal() and its companions accept an infinite \(\psi\) and evaluate the distribution the ideal one converges to, so the prediction needs no special handling before it is passed on:

print(dvmideal(seq(5L, 1L), 5.5, psi_faint))
#> [1] 0.08520507 0.18425125 0.23557762 0.21214074 0.14471281

Those are the probabilities of the geometric model with a population index of \(r = 10^{0.4}\), which is what an unbounded \(\psi\) means:

print(identical(
    dvmideal(seq(5L, 1L), 5.5, psi_faint),
    dvmgeom(seq(5L, 1L), 5.5, 10^0.4)
))
#> [1] TRUE

Just short of that point the mean moves so little with \(\psi\) that the iteration can run out of steps. vmideal_glm() then warns instead of returning the value it happened to stop at; the estimate is unreliable there, and its standard error does not show it.

For a single global \(\psi\) the two methods of the previous sections remain preferable — maximum likelihood for accuracy, the transformation for speed. The benefit of the linear model appears once covariates are added. Because the fit is an ordinary glm object, summary(), anova(), AIC()/BIC() and select_knots() can all be used. A model in which \(\psi\) varies with solar longitude would be written as

vmideal_glm(cbind(magn, lim_magn) ~ sl, data = magnitudes, weights = Freq)

and a smooth activity-dependent profile as

vmideal_glm(cbind(magn, lim_magn) ~ splines::ns(sl, df = 3), data = magnitudes, weights = Freq)

Since the family reports the exact log-likelihood of the fitted distribution, AIC() is not restricted to comparing models of the same kind. The geometric model of visual meteor magnitudes can be fitted to the same data with vmgeom_glm() and compared directly:

result_geom <- vmgeom_glm(
    cbind(magn, lim_magn) ~ 1,
    data = magnitudes_obs,
    weights = magnitudes_obs$Freq
)
print(c("ideal" = AIC(result_glm), "geometric" = AIC(result_geom)))
#>     ideal geometric 
#>  350.6568  353.1941

Both models have one parameter here, so the comparison reduces to their likelihoods. The lower value belongs to the ideal distribution, but the two lie close enough together that this excerpt of 97 meteors does not decide between them; a difference of a few units is weak evidence. The comparison is only meaningful because both fits use the same observations and the same response — a model fitted to a subset, or with the zero-frequency rows retained, is not comparable in this way.

Residual Analysis

So far, we have operated under the assumption that the real distribution of meteor magnitudes is exponential and that the perception probabilities are accurate. We now use the Chi-Square goodness-of-fit test to check whether the observed frequencies match the expected frequencies.

The maximum likelihood estimate of the first section is used for this. What the test is meant to reveal is whether the model itself describes the data, and any inefficiency of the estimator would enter the result alongside it. Since the maximum likelihood estimate is the one that extracts all the information the magnitudes carry about \(\psi\), a departure found here can be attributed to the model rather than to the way the parameter was obtained. The correction of the degrees of freedom applied below rests on the same assumption.

Using the estimated parameter, we retrieve the relative frequencies p for each observation and add them to the data frame magnitudes:

psi_mean <- result_ml$par
magnitudes$p <- with(magnitudes, dvmideal(m = magn, lm = lim_magn, psi_mean))

We must also consider the probabilities for the magnitude class with the brightest meteors.

magn_min <- min(magnitudes$magn)

The smallest magnitude class magn_min is -6. In calculating the probabilities, we assume that the magnitude class -6 contains meteors that are either brighter or equally bright as -6 and thus use the function pvmideal() to determine their probability.

idx <- magnitudes$magn == magn_min
magnitudes$p[idx] <- with(
    magnitudes[idx, ],
    pvmideal(m = magn + 1L, lm = lim_magn, psi_mean, lower.tail = TRUE)
)

This ensures that the probability of observing a meteor of any given magnitude is 100%. This is known as the normalization condition. Accordingly, the Chi-Square goodness-of-fit test will fail if this condition is not met.

We now create the contingency table magnitutes_observed for the observed meteor magnitudes and its margin table.

magnitutes_observed <- xtabs(Freq ~ magn_id + magn, data = magnitudes)
magnitutes_observed_mt <- margin.table(magnitutes_observed, margin = 2)
print(magnitutes_observed_mt)
#> magn
#>   -6   -5   -4   -3   -2   -1    0    1    2    3    4    5    6 
#>  0.0  0.0  0.0  0.0  3.0  4.0  7.0 10.0 23.0 26.5 20.0  3.0  0.5

Next, we check which magnitude classes need to be aggregated so that each contains at least 10 meteors, allowing us to perform a Chi-Square goodness-of-fit test.

The last output shows that meteors of magnitude class 0 or brighter must be combined into a magnitude class 0-. Meteors with a brightness less than 4 are grouped here in the magnitude class 4+, and a new contingency table magnitudes.observed is created:

magnitudes$magn[magnitudes$magn <= 0] <- "0-"
magnitudes$magn[magnitudes$magn >= 4] <- "4+"
magnitutes_observed <- xtabs(Freq ~ magn_id + magn, data = magnitudes)
print(margin.table(magnitutes_observed, margin = 2))
#> magn
#>   0-    1    2    3   4+ 
#> 14.0 10.0 23.0 26.5 23.5

We now need the corresponding expected relative frequencies

magnitutes_expected <- xtabs(p ~ magn_id + magn, data = magnitudes)
magnitutes_row_freq <- margin.table(magnitutes_observed, margin = 1)
magnitutes_expected <- sweep(magnitutes_expected, 1, magnitutes_row_freq, `*`)
magnitutes_expected <- magnitutes_expected / sum(magnitutes_expected)
print(sum(magnitudes$Freq) * margin.table(magnitutes_expected, margin = 2))
#> magn
#>       0-        1        2        3       4+ 
#> 14.76013 15.18671 21.91264 23.18176 21.95877

and then carry out the Chi-Square goodness-of-fit test:

chisq_test_result <- chisq.test(
    x = margin.table(magnitutes_observed, margin = 2),
    p = margin.table(magnitutes_expected, margin = 2)
)

As a result, we obtain the p-value:

chi2_df <- chisq_test_result$parameter - 1
chi2_pval <- pchisq(chisq_test_result$statistic, df = chi2_df, lower.tail = FALSE)
print(chi2_pval)
#> X-squared 
#> 0.4848224

If we set the level of significance at 5 percent, then it is clear that the p-value with 0.4848224 is greater than 0.05. Thus, under the assumption that the magnitude distribution follows the ideal meteor magnitude distribution and that the perception probabilities are correct (i.e., error-free or precisely known), these assumptions cannot be rejected. However, the converse is not true; the assumptions may not necessarily be correct. The total count of meteors here is too small for such a conclusion.

To verify the p-value, we also graphically represent the Pearson residuals:

chisq_test_residuals <- with(new.env(), {
    chisq_test_residuals <- residuals(chisq_test_result)
    v <- as.vector(chisq_test_residuals)
    names(v) <- names(chisq_test_residuals)
    v
})

plot(
    chisq_test_residuals,
    main = "Residuals of the chi-square goodness-of-fit test",
    xlab = "m",
    ylab = "Residuals",
    ylim = c(-3, 3),
    xaxt = "n"
)
abline(h = 0.0, lwd = 2)
axis(1, at = seq_along(chisq_test_residuals), labels = names(chisq_test_residuals))