Stochastic portfolio optimisation and out-of-sample testing using R

Author: Andreï Victorovitch Kostyrka, University of Luxembourg.

Last updated: 2018-07-31.

One might ask a question, “What is it like to try to pick an optimal portfolio and invest in stocks?” Well, here I have prepared some R code that does the following thing:

  • Downloads stock data from Yahoo Finance from 2014 for a selection of stocks;
  • Puts all stocks together across the maximum set of available dates and nicely imputes missing values using Kalman filter with structural modelling;
  • Tries all possible combinations or portfolios (as many as possible, using Halton pseudo-random low-discrepancy sequences, doing many iterations and saving the best result from every iteration to save memory);
  • Creates amazing colourful plots for subsequent analysis;
  • Returns statistics for equally weighted portfolios, max-return under equally weighted risk, min-risk under equally weighted return, and maximum Sharpe ratio (for simplicity, the ratio of returns to risk, assuming there is no risk-free asset);
  • Tests these 4 portfolios out of sample for 1 year (“if one started trading one year ago, what would one have obtained”).

Two sets of stocks were chosen: most lucrative U.S. companies and most lucrative Russian companies. This is what one gets if one chooses 13 top U.S. companies:

library(BatchGetSymbols)
library(zoo)
library(imputeTS) # For imputation
library(randtoolbox) # For Sobel sampling
library(parallel)
 
# http://fortune.com/2017/06/07/fortune-500-companies-profit-apple-berkshire-hathaway/
# https://www.swiftpassportservices.com/blog/profitable-companies-russia/
 
syms <- c("GOOG", "AAPL", "JPM", "BRK-A", "WFC", "MCD", "BAC", "MSFT", "AMZN", "JNJ", "C", "MO", "GE")
symnames <- c("Google", "Apple", "J.P. Morgan", "Berkshire Hathaway", "Wells Fargo", "McDonald's",
              "Bank of America", "Microsoft", "Amazon", "Johnson and Johnson", "Citigroup", "Altria group", "General Electric")
cat("Calculating optimal portfolio for US stocks\n")
 
# syms <- c("SGTZY", "ROSN.ME", "OGZPY", "LUKOY", "SBRCY", "TRNFP.ME", "NILSY", "ALRS.ME", "YNDX")
# symnames <- c("SurgutNefteGas", "RosNeft", "GazProm", "LukOil", "SberBank", "TransNeft",
#               "Norilsk Nickel", "Alrosa", "Yandex")
# cat("Calculating optimal portfolio for Russian stocks\n")
 
first.date <- as.Date("2014-01-01")
last.date <- as.Date("2018-08-01")
end.ins <- as.Date("2017-08-01")
 
dat <- BatchGetSymbols(tickers = syms, first.date = first.date, last.date = last.date)
 
allt <- dat$df.tickers
allt <- allt[!is.na(allt$price.adjusted), ]
dates <- sort(unique(allt$ref.date))
prices <- data.frame(dates)
prices[, 2:(length(syms)+1)] <- NA
colnames(prices)[2:(length(syms)+1)] <- syms
for (i in syms) {
  datesi <- allt$ref.date[as.character(allt$ticker)==i]
  pricesi <- allt$price.adjusted[as.character(allt$ticker)==i]
  prices[match(datesi, dates), i] <- pricesi
  prices[, i] <- zoo(prices[, i], order.by = prices$dates)
  prices[, i] <- na.kalman(prices[, i])
}
prices2 <- prices
prices[,1] <- NULL
 
co <- rainbow(length(syms), end=0.75)
png("1-relative-prices.png", 700, 400, type="cairo")
plot(prices[,1]/mean(prices[,1]), col=co[1], lwd=1.5, main="Prices divided by their in-sample mean", xlab="Time", ylab="Ratio")
for (i in 2:length(syms)) {
  lines(prices[,i]/mean(prices[,i]), col=co[i])
}
legend("topleft", syms, col=co, lwd=1)
dev.off()
 
rs <- data.frame(rep(NA, nrow(prices)-1))
for (i in syms) {
  rs[, i] <- diff(log(prices[, i]))
}
rs[,1] <- NULL
 
w0 <- rep(1/length(syms), length(syms))
names(w0) <- syms
rmean <- zoo(as.numeric(as.matrix(rs) %*% w0), order.by = time(rs[,1]))
 
a <- table(as.numeric(format(time(rmean), "%Y")))
daysperyear <- mean(a[1:(length(a)-1)]) # Because the last year is incomplete
 
rsfull <- rs # Full sample
rs <- rs[time(rs[,1]) <= end.ins, ] # Training sample
rsnew <- rsfull[time(rsfull[,1]) > end.ins, ] # Test sample
 
meanrs <- colMeans(rs)
covrs <- cov(rs)
 
musigma <- function(w) {
  mu <- sum(meanrs * w)*daysperyear
  s <- sqrt((t(w) %*% covrs %*% w) * daysperyear)
  return(c(mu=mu, sigma=s, ratio=mu/s))
}
 
m0 <- musigma(w0)
 
num.workers <- if (.Platform$OS.type=="windows") 1 else detectCores()
MC <- 108000
iters <- 100
cc <- 150 # How many colours
cols <- rainbow(cc, end = 0.75)
sharpecuts <- seq(-0.1, 1.5, length.out = cc)
 
png("2-portfolios.png", 700, 600, type="cairo")
 
# First iteration
cat("Calculating", MC, "portfolios for the first time.\n")
ws <- halton(MC, length(syms), init=TRUE)
ws <- ws/rowSums(ws)
colnames(ws) <- syms
res <- mclapply(1:MC, function(i) musigma(ws[i, ]), mc.cores = num.workers)
res <- as.data.frame(matrix(unlist(res, use.names = FALSE), nrow=MC, byrow = TRUE))
colnames(res) <- c("mean", "sd", "ratio")
prcmu0 <- ecdf(res$mean)(m0[1]) # Which quantile is the mean
prcsd0 <- ecdf(res$sd)(m0[2]) # Which quantile is the sd
qmplus <- quantile(res$mean, prcmu0+0.005)
qmminus <- quantile(res$mean, prcmu0-0.005)
qsplus <- quantile(res$sd, prcsd0+0.005)
qsminus <- quantile(res$sd, prcsd0-0.005)
meanvicinity <- which(res$mean < qmplus & res$mean > qmminus)
sdvicinity <- which(res$sd < qsplus & res$sd > qsminus)
 
plot(res[,2:1], pch=16, col=cols[as.numeric(cut(res$ratio, breaks = sharpecuts))],
     xlim=range(res$sd)+c(-0.03, 0.03), ylim=pmax(range(res$mean)+c(-0.03, 0.03), c(0, -10)),
     main="Risk and return for various portfolios", xlab="Annualised volatility", ylab="Annualised log-return")
 
sharpes <- mus <- sds <- array(NA, dim=c(iters+1, length(syms)))
sharpes[1, ] <- ws[which.max(res$ratio), ]
mus[1, ] <- ws[which(res$mean == max(res$mean[sdvicinity])), ]
sds[1, ] <- ws[which(res$sd == min(res$sd[meanvicinity])), ]
pairsm <- pairss <- array(NA, dim=c(iters+1, 3))
pairsm[1, ] <- res$mean[c(which.max(res$ratio), which(res$mean == max(res$mean[sdvicinity])), which(res$sd == min(res$sd[meanvicinity])))]
pairss[1, ] <- res$sd[c(which.max(res$ratio), which(res$mean == max(res$mean[sdvicinity])), which(res$sd == min(res$sd[meanvicinity])))]
 
for (i in 1:iters) {
  cat("Calculating", MC, "portfolios, iteration", i, "out of", iters, "\n")
  # ws <- sobol(MC, length(syms), scrambling = 1, init=FALSE) # There is a bug in randtoolbox: it starts returning numbers outside [0, 1]
  ws <- halton(MC, length(syms), init=FALSE)
  ws <- ws/rowSums(ws)
  colnames(ws) <- syms
  res <- mclapply(1:MC, function(i) musigma(ws[i, ]), mc.cores = num.workers)
  res <- as.data.frame(matrix(unlist(res, use.names = FALSE), nrow=MC, byrow = TRUE))
  colnames(res) <- c("mean", "sd", "ratio")
  meanvicinity <- which(res$mean < qmplus & res$mean > qmminus)
  sdvicinity <- which(res$sd < qsplus & res$sd > qsminus)
  maxsh <- which.max(res$ratio)
  maxm <- which(res$mean == max(res$mean[sdvicinity]))
  minsd <- which(res$sd == min(res$sd[meanvicinity]))
  sharpes[i+1, ] <- ws[maxsh, ]
  mus[i+1, ] <- ws[maxm, ]
  sds[i+1, ] <- ws[minsd, ]
  pairsm[i+1, ] <- res$mean[c(maxsh, maxm, minsd)]
  pairss[i+1, ] <- res$sd[c(maxsh, maxm, minsd)]
  points(res[,2:1], pch=16, col=cols[as.numeric(cut(res$ratio, breaks = sharpecuts))])
  rm(res); gc()
}
 
points(as.numeric(pairss), as.numeric(pairsm))
 
sharpems <- apply(sharpes, 1, musigma)
wmaxsharpe <- sharpes[which.max(sharpems[3,]), ]
meanms <- apply(mus, 1, musigma)
wmaxmean <- mus[which.max(meanms[1,]), ] # Maximum returns while SD is the same
sdms <- apply(sds, 1, musigma)
wminsd <- sds[which.min(sdms[2,]), ] # Minimum SD while mean is the same
 
m1 <- musigma(wmaxsharpe)
m2 <- musigma(wmaxmean)
m3 <- musigma(wminsd)
finalstats <- cbind(rbind(w0, wmaxsharpe, wmaxmean, wminsd), rbind(m0, m1, m2, m3))
rownames(finalstats) <- c("Equal", "MaxSharpe", "MaxReturn", "MinRisk")
colnames(finalstats)[(length(syms)+1):(length(syms)+3)] <- c("RInS", "VInS", "SRInS")
 
 
points(m0[2], m0[1], cex=3, pch=16)
points(c(m1[2], m2[2], m3[2]), c(m1[1], m2[1], m3[1]), cex=2, pch=16)
dev.off()
 
# And now checking these revenues on the test sample
r0 <- zoo(as.numeric(as.matrix(rsnew) %*% w0), order.by = time(rsnew[,1]))
r1 <- zoo(as.numeric(as.matrix(rsnew) %*% wmaxsharpe), order.by = time(rsnew[,1]))
r2 <- zoo(as.numeric(as.matrix(rsnew) %*% wmaxmean), order.by = time(rsnew[,1]))
r3 <- zoo(as.numeric(as.matrix(rsnew) %*% wminsd), order.by = time(rsnew[,1]))
 
png("3-test-sample-returns.png", 700, 400, type="cairo")
plot(r0, main="Out-of-sample returns of weighted portfolios", ylab="Return", xlab="Time")
lines(r1, col=2)
lines(r2, col=3)
lines(r3, col=4)
legend("topright", c("Equal", "Sharpe", "Max. return", "Min. volatility"), col=1:4, lwd=1)
dev.off()
 
png("4-test-sample.png", 700, 400, type="cairo")
plot(exp(cumsum(r0)), ylim=c(0.6, 1.4), main="Portfolio growth over the last year in the test sample", ylab="Relative growth", xlab="Time")
abline(h=1)
lines(exp(cumsum(r1)), col=2)
lines(exp(cumsum(r2)), col=3)
lines(exp(cumsum(r3)), col=4)
legend("bottomleft", c("Equal", "Sharpe", "Max. return", "Min. volatility"), col=1:4, lwd=1)
dev.off()
 
finals <- cbind(r0, r1, r2, r3)
meanf <- colMeans(finals)*daysperyear
sdf <- apply(finals, 2, sd)*sqrt(daysperyear)
finalsoos <- cbind(meanf, sdf, meanf/sdf)
colnames(finalsoos) <- c("ROOS", "VOOS", "SRROS")
finalstats <- cbind(finalstats, finalsoos)
colnames(finalstats)[1:length(syms)] <- symnames
 
library(stargazer)
stargazer(t(finalstats), summary = FALSE, type="html", out="a.html")
 
write.csv(prices2, "stocks.csv", row.names = FALSE)

This is how the 13 U.S. stocks behaved.

Relative prices of top U.S. stocks

This is how 10 million random portfolios behaved (black dots indicate 100 best portfolios under the three criteria; big black dot for the equally weighted portfolio).

Return and risk of various U.S. portfolios

These are portfolio weights for U.S. companies with their performance.

Equal MaxSharpe MaxReturn MinRisk
Google 0.077 0.011 0.004 0.052
Apple 0.077 0.132 0.128 0.105
J.P. Morgan 0.077 0.133 0.099 0.028
Berkshire Hathaway 0.077 0.019 0.040 0.170
Wells Fargo 0.077 0.003 0.008 0.025
McDonald's 0.077 0.164 0.148 0.192
Bank of America 0.077 0.030 0.034 0.036
Microsoft 0.077 0.078 0.180 0.022
Amazon 0.077 0.088 0.161 0.005
Johnson & Johnson 0.077 0.138 0.064 0.138
Citigroup 0.077 0.003 0.003 0.009
Altria group 0.077 0.198 0.131 0.142
General Electric 0.077 0.003 0.001 0.078
Return in-sample 0.145 0.179 0.190 0.145
Risk in-sample 0.142 0.128 0.141 0.119
Sharpe ratio in-sample 1.023 1.395 1.345 1.217
Return in 2017.07–2018.06 0.135 0.163 0.241 0.053
Risk in 2017.07–2018.06 0.144 0.134 0.146 0.130
Sharpe ratio in 2017.07–2018.06 0.941 1.218 1.657 0.404

This is how these four U.S. portfolios look out of sample.

Returns for different optimal U.S. portfolios out-of-sample

If one had invested in these four portfolios a year before, this is what one would have harvested by now.

Cumulative growth of one's investments into U.S. stocks


And this is the result for top 9 Russian companies.

This is how the 9 Russian stocks behaved.

Relative prices of top Russian stocks

This is how 10 million random portfolios behaved (black dots indicate 100 best portfolios under the three criteria; big black dot for the equally weighted portfolio).

Return and risk of various Russian portfolios

Note that some portfolios can actually yield negative returns, and the volatility is generally higher!

These are portfolio weights for Russian companies with their performance.

Equal MaxSharpe MaxReturn MinRisk
SurgutNefteGas 0.111 0.005 0.009 0.175
RosNeft 0.111 0.162 0.031 0.269
GazProm 0.111 0.012 0.068 0.067
LukOil 0.111 0.018 0.083 0.037
SberBank 0.111 0.038 0.031 0.014
TransNeft 0.111 0.248 0.016 0.213
Norilsk Nickel 0.111 0.079 0.381 0.044
Alrosa 0.111 0.413 0.358 0.049
Yandex 0.111 0.026 0.022 0.131
Return in-sample 0.055 0.227 0.172 0.055
Risk in-sample 0.245 0.218 0.245 0.218
Sharpe ratio in-sample 0.223 1.042 0.701 0.250
Return in 2017.07–2018.06 0.202 0.164 0.223 0.164
Risk in 2017.07–2018.06 0.179 0.170 0.215 0.153
Sharpe ratio in 2017.07–2018.06 1.129 0.965 1.039 1.067

This is how these four Russian portfolios look out of sample.

Returns for different optimal Russian portfolios out-of-sample

If one had invested in these four portfolios a year before, this is what one would have harvested by now.

Cumulative growth of one's investments into Russian stocks

So actually if one used this investing strategy based on the information available in mid-2017, by now, depending on the criterion (Sharpe ratio or maximum profitability) they would have earned 14–22% on Russian stocks or 15–17% on American stocks!

TODO: constrained optimisation using the best stochastic solution as a starting point.

<...>

Why are you still here? Why are you not trading yet?

Using Twitter to model voters’ attitude towards presidential candidates: a case for Russia in 2018

NB. Any opinions that might be critical of the political system of Russia are justified by the fact that the author is a citizen of Russia himself, and retains the right to give opinions, or commentary, or judgement.

Introduction

Twitter has been known to serve as a news publication machine that drives the attention of mass media and shapes the preferences of voters; a good example of this would be the presidential elections in the USA in 2016 (Kapko, 2016). Presidential candidates were using this medium to communicate to voters directly and express their opinions outside presidential debates. It has been speculated that predictions made based on Twitter data can perform better than those obtained by robocalls or by examining the distribution of lawn signs.

Sometimes, as in the case with Donald Trump, using free-wheeling speech style can be really beneficial because it reassures voters that their candidates speaks what (s)he thinks. Candidates' tweets are news per se, and news agencies can easily get the low-hanging fruit by just re-publishing them and giving their own interpretation. Besides that, Twitter provides an opportunity to engage into a direct dialogue between the candidates and voters via the feature of directed tweets. Celebrities and influential people often respond directly to the candidates, which drives the evolution of voters' views. It has been noted that often it is clear who is writing the tweet: the candidate him/herself or his/her staffers.

As good as it may sound, there are obvious drawbacks to Twitter. First and foremost, it seems almost impossible to fit a meaningful argument within the limit of 140 (now 280) characters. As a result, it causes oversimplification of discourse. Another point is debatable: as soon as something is posted on Twitter, it is impossible to fully withdraw the tweet if someone else has noticed it. This can be both a drawback and a strong point: as soon as lewd material is posted, the poster bears full responsibility for it, and experiences all potential backlash.

Twitter as a predictor for election results has been rigorously analysed by researchers in the past. In fact, such analyses were applied to the 2010 Australian elections (Burgess and Bruns, 2012), 2010 Swedish elections (Larsson and Moe, 2012), 2011 Dutch elections (Sang and Bos, 2012), 2012 USA elections (Wang et al., 2012), 2015 Venezuelan elections (Yang, Macdonald and Ounis, 2017), 2016 USA elections (Bessi and Ferrara, 2016) etc. Some of them are using convolutional neural networks, some of them are applying clever tools to avoid misclassification, but some of them rely on a simple count of words from positive and negative dictionaries. We are going to adopt this simple approach in an attempt to analyse Russian data and evaluate the balance of powers (or lack thereof) in Russia.

In this work, I am using big data (70 000 tweets in total) and big data processing tools (Google Translate API) to investigate the relative popularity of candidates in the Russian presidential election of 2018. The methodology will be similar to that of Bhatia (2015).

Methodology

In this work, I develop a simple method to analyse the sentiment towards election candidates in arbitrary languages using Google Translate. It should be noted that as the moment of writing, Google is offering a free $300 trial for its Cloud API, so the author made use of his free access to the machine translation services. The workflow is as follows:

  1. Using R and twitteR package, obtain a large number (10 000) tweets mentioning the candidate;
  2. Break the tweets into small batches (100 tweets per batch separated with triple paragraphs) and write scripts for translation;
  3. Using Google Cloud Translation API, run the translation scripts and obtain the server response as a JSON object;
  4. Input the JSON object into R, break it into separate tweets again and clean up the text;
  5. Count the positive and negative words in those tweets using dictionaries and analyse the distributions.

Obviously, the main drawback of the simple dictionary match is the lack of context. There are examples of why this might return wrong results. Example 1: “Mr. X is going to put an end to corruption!” This author has a positive attitude towards Mr. X, but due to the presence of a negative word (“corruption”), is going to be counted as negative. Example 2: “@newsagency Yes, of course, Mr. Y is going to improve the situation, yeah, like, thousand times!” This is an example of irony; the attitude of this tweet is negative and sceptical, but the simple count will find a positive match (“improve”) and count this tweet as positive. Example 3: “Mr. Z is not honest!”

To counter these three points, an argument should be made. The error from the first category does not change much in terms of emotional attitude; if candidate X is viewed as a corruption fighter, the writer implies that the situation in the country is grave (hence the need to fight), and this negative score should be counted towards the general level of negativity, not just the negativity associated with candidate X. If candidates Y and Z also claim they are going to fight corruption, then all these negative votes will contribute to a fixed proportion of false negatives (say, n%), and the ratio of negative to positive votes should not change. This implies that ratios, not absolute values should be examined. The error from the second category is so semantically complicated that is it improbable that any advanced methods (including machine learning and neural networks) based on pure text analysis are going to detect irony and correctly interpret it in a negative sense. However, the proportion of such tweets is thought to be low, and an advanced user-based analysis is suggested in this case: if a user is generally opposing candidate Y (or is pessimistic in general), then any tweets with positive score about candidate Y should be re-weighted or discarded as “implausible”. The error from the third category, however, can be remedied with the use of multi-layered neural network or contextual analysis algorithms with look-ahead and look-behind: if the word from the “positive” list is preceded by “not”, “no”, “barely” or followed by “my a**” or other similar modifiers, the match should be counted for the “negative” list, and vice versa.

Implementation

Searching for tweets

Assumption 1. During the period from 2018-01-01 to 2018-03-18, any tweet containing the surname of a candidate is meant to be about the candidate, not his namesake.

This assumption allows us to identify tweets about the presidential candidates by a simple search using the name of the candidate. The candidates seem to have quite unique surnames, and almost all media covering people with their surnames are covering namely those people.

Assumption 2. There is no bias in Twitter search results; i. e., Twitter is not being in favour of any candidate, or is not filtering negative search results, or is not affected by spam bots who praise candidates.

This assumption is crucial for the unbiasedness of our results; so far, there have been no signs of Twitter censorship in Russia. However, there might be a population bias: people who are using Twitter can turn out to be younger and more progressive, and therefore, more opposing to the current political leader, Putin. It means that whereas most tweets about Putin on Twitter can come from a minority of political opposition, the majority of Putin voters are budget-depending elderly people and beneficiaries who live in faraway regions without internet access or any desire to express their opinions publicly.

api_key <- "..."
api_secret <- "..."
access_token <- "..."
access_token_secret <- "..."
 
library(twitteR)
setup_twitter_oauth(api_key, api_secret, access_token, access_token_secret)
 
find1 <- "Путин"
find2 <- "Бабурин"
find3 <- "Грудинин"
find4 <- "Жириновский"
find5 <- "Собчак"
find6 <- "Сурайкин"
find7 <- "Титов"
find8 <- "Явлинский"
find9 <- "Навальный"
finds <- c(find1, find2, find3, find4, find5, find6, find7, find8, find9)
 
number <- 10000
 
tweet1 <- searchTwitter(find1, n=number, lang="ru", since="2018-01-01", until="2018-03-18")
tweet2 <- searchTwitter(find2, n=number, lang="ru", since="2018-01-01", until="2018-03-18")
tweet3 <- searchTwitter(find3, n=number, lang="ru", since="2018-01-01", until="2018-03-18")
tweet4 <- searchTwitter(find4, n=number, lang="ru", since="2018-01-01", until="2018-03-18")
tweet5 <- searchTwitter(find5, n=number, lang="ru", since="2018-01-01", until="2018-03-18")
tweet6 <- searchTwitter(find6, n=number, lang="ru", since="2018-01-01", until="2018-03-18")
tweet7 <- searchTwitter(find7, n=number, lang="ru", since="2018-01-01", until="2018-03-18")
tweet8 <- searchTwitter(find8, n=number, lang="ru", since="2018-01-01", until="2018-03-18")
tweet9 <- searchTwitter(find9, n=number, lang="ru", since="2018-01-01", until="2018-03-18")
 
save(tweet1, tweet2, tweet3, tweet4, tweet5, tweet6, tweet7, tweet8, tweet9, file="tweets.RData")

Breaking the tweets for translation

A simple snippet of code is suggested below to translate the long and heavy objects on Google Translate. As we know, Google charges API users on a character basis (as of writing, $20 per 1 million characters), with virtually no limit for translation (if the proper quotas are set to be infinite). However, just in order to be sure that the translations are made in time and no information is lost, we broke the messages into blocks of 100 tweets.

byhow <- 100
 
for (cand in 1:9) {
  texts <- get(paste0("tweet", cand))
  blocks <- ceiling(length(get(paste0("tweet", cand)))/byhow)
  for (i in 1:blocks) {
    sink(paste0("scripts", cand, "/scr", i, ".sh"))
    fr <- byhow*(i-1)+1
    to <- byhow*(i-1)+byhow
    cat('
    curl -s -X POST -H "Content-Type: application/json" \\
    -H "Authorization: Bearer "$(gcloud auth print-access-token) \\
    --data "{
      \'q\': \'
      ')
      if (to > length(texts)) to <- length(texts)
      for (j in fr:to) {
        cat("\n\n\n")
        a <- texts[[j]]$text
        a <- gsub("['\"`]", "", a)
        cat(a)
      }
      cat('
      \',
      \'source\': \'ru\',
      \'target\': \'en\',
      \'format\': \'text\'
    }" "https://translation.googleapis.com/language/translate/v2"
 
    ')
    sink()
  }
 
}

Therefore, for each of the nine candidates, it will write out up to 100 script files into a corresponding folder.

Using Google Translate API

Once the script files, titled scr1.sh, ..., scr100.sh are ready, all that remains is run them in series with a time-out (since Google API has a character-per-minute quota).

#!/bin/bash
for i in {1..100}
  do
  echo "Processing tweet file $i of 100"
  ./scr$i.sh > out$i.txt
  sleep 12
done

This script must be run in every folder (1–9), and the translation results will be stored in files out1.txt, ..., out100.txt (or up to the number of tweets divided by 100 if the candidate was not very popular).

Merging the objects into tweet lists

A simple for loop (for 9 candidates, from 1 to the number of output files) is required to merge the data back.

library(jsonlite)
 
alltrans <- list()
 
for (cand in 1:9) {
  lf <- list.files(paste0("./scripts", cand, "/"))
  lf <- lf[grep("out", lf)]
  howmany <- length(lf)
 
  alltrans[[cand]] <- list()
 
  for (i in 1:howmany) {
    a <- fromJSON(readLines(paste0("scripts", cand, "/out", i, ".txt")))
    a <- a[[1]]$translations$translatedText
    aa <- strsplit(a, "\n\n\n")[[1]]
    aa <- gsub("@ +", "@", aa)
    aa <- gsub("^\n +", "", aa)
    aa <- aa[aa!=""]
 
    alltrans[[cand]][[i]] <- aa
    if (! i%%10) print(paste0("Candidate ", cand, ": ", i, " out of ", howmany))
  }
}
 
clean <- function(t) {
  t <- gsub('[[:punct:]]', '', t)
  t <- gsub('[[:cntrl:]]', '', t) 
  t <- gsub('\\d+', '', t)
  t <- gsub('[[:digit:]]', '', t)
  t <- gsub('@\\w+', '', t)
  t <- gsub('http\\w+', '', t)
  t <- gsub("^\\s+|\\s+$", "", t)
  t <- gsub('"', '', t)
  t <- strsplit(t,"[ \t]+")
  t <- lapply(t, tolower)
  return(t)
}
 
alltrans2 <- lapply(alltrans, unlist)
 
alltrans3 <- lapply(alltrans2, clean)

As we see, this clean-up included removing punctuation, control characters, URLs, and digits from the tweets, and then splitting them at spaces or tabs. For matching, conversion to lowercase was used.

Analysing sentiments

First and foremost, let us look at the distributions of positive and negative tweet counts for various candidates.

positive <- scan('positive-words.txt', what='character', comment.char=';')
negative <- scan('negative-words.txt', what='character', comment.char=';')
 
posscore <- function(tweet) {
pos.match <- match(tweet, positive)
pos.match <- !is.na(pos.match)
pos.score <- sum(pos.match)
return(pos.score)
}
 
negscore <- function(tweet) {
neg.match <- match(tweet, negative)
neg.match <- !is.na(neg.match)
neg.score <- sum(neg.match)
return(neg.score)
}
 
posscores <- list()
negscores <- list()
for (i in 1:9) {
posscores[[i]] <- unlist(lapply(alltrans3[[i]], posscore))
negscores[[i]] <- unlist(lapply(alltrans3[[i]], negscore))
}
 
poscounts <- lapply(posscores, table)
negcounts <- lapply(negscores, table)
 
cols <- rainbow(10)
 
cairo_pdf("counts1.pdf", 7, 5)
plot(NULL, NULL, xlim=c(0, 6.7), ylim=c(0, 7516), main = "Positive tweet counts for candidates", ylab = "Tweets", xlab="Positive words in a tweet")
for (j in 1:9) {
for (i in 1:length(poscounts[[j]])) {
lines(c(i-1 + (j-1)/10, i-1 + (j-1)/10), c(0, poscounts[[j]][i]), lwd=5, col=cols[j])
}
}
legend("topright", finds, lwd=5, col=cols)
dev.off()
 
cairo_pdf("counts2.pdf", 7, 5)
plot(NULL, NULL, xlim=c(0, 6.7), ylim=c(0, 7516), main = "Negative tweet counts for candidates", ylab = "Tweets", xlab="Negative words in a tweet")
for (j in 1:9) {
for (i in 1:length(negcounts[[j]])) {
lines(c(i-1 + (j-1)/10, i-1 + (j-1)/10), c(0, negcounts[[j]][i]), lwd=5, col=cols[j])
}
}
legend("topright", finds, lwd=5, col=cols)
dev.off()

Distribution of positive word counts

Distribution of negative word counts

Fig. 1. Distribution of tweets

Since there is no real political competition to Putin (position 1) among the present candidates (positions 2–8), let us pay some attention to his closest political rival who was denied access to the elections, Navalny (position 9). As we can see from Fig. 1, the number of tweets related to Putin and containing 1–2 positive words is smaller than that related to Navalny. At the very same time, paradoxically, the number of tweets related to Putin and containing 1–2 negative words is also smaller than that of Navalny.

totpos <- numeric(9)
totneg <- numeric(9)
for (i in 1:9) {
totpos[i] <- sum(poscounts[[i]] * as.numeric(names(poscounts[[i]])))
totneg[i] <- sum(negcounts[[i]] * as.numeric(names(negcounts[[i]])))
}
 
# Ordering candidates not alpabetically but as they are in the database
a <- factor(finds)
finds <- factor(a, levels(a)[c(5, 1, 2, 3, 6, 7, 8, 9, 4)])
 
ratios <- totpos/totneg
ratiosdf <- data.frame(ratio=ratios, name=finds)
 
 
library(ggplot2)
p <- ggplot(data=ratiosdf, aes(x=name, y=ratio, fill=name)) + geom_bar(stat="identity") + ylab("Positive/negative ratio") +
theme_minimal() + coord_flip() + scale_x_discrete(limits = rev(levels(ratiosdf$name))) + theme(legend.position="none", axis.title.y=element_blank())
cairo_pdf("ratios.pdf", 8, 7)
plot(p)
dev.off()

Ratio of positive to negative tweet counts

Fig. 2. Tweet sentiment ratios for candidates

It is unsurprising that that the ratio for Navalny, who is known as a fighter against corruption, is lower than that of Putin, despite the fact that there is a substantial number of opposing bloggers, due to the fact that the word “corruption” is negative per se. In general, only one candidate (Grudinin) has this ratio greater or equal to one. This means that tweets containing the names of political leaders are negative in tone in general.

Now it it time to see which words were the top positive and top negative for each candidate.

poswords <- function(tweets) {
pmatch <- match(t, positive)
posw <- positive[pmatch]
posw <- posw[!is.na(posw)]
return(posw)
}
 
negwords <- function(tweets) {
pmatch <- match(t, negative)
negw <- negative[pmatch]
negw <- negw[!is.na(negw)]
return(negw)
}
 
pdata <- list()
ndata <- list()
for (j in 1:9) {
pdata[[j]] <- ""
ndata[[j]] <- ""
for (t in alltrans3[[j]]) {
pdata[[j]] <- c(poswords(t), pdata[[j]])
ndata[[j]] <- c(negwords(t), ndata[[j]])
}
}
 
pfreqs <- lapply(pdata, table)
nfreqs <- lapply(ndata, table)
 
dpfreq <- list()
dnfreq <- list()
for (j in 1:9) {
thresh <- length(alltrans2[[j]])/200
a <- pfreqs[[j]]
a <- a[a>thresh]
b <- nfreqs[[j]]
b <- b[b>thresh]
dpfreq[[j]] <- data.frame(word=as.character(names(a)), freq=as.numeric(a))
dpfreq[[j]]$word <- as.character(dpfreq[[j]]$word)
dnfreq[[j]] <- data.frame(word=as.character(names(b)), freq=as.numeric(b))
dnfreq[[j]]$word <- as.character(dnfreq[[j]]$word)
}
 
for (j in 1:9) {
p1 <- ggplot(dpfreq[[j]], aes(word, freq)) + geom_bar(stat="identity", fill="lightblue") + theme_bw() +
geom_text(aes(word, freq, label=freq), size=4) + labs(x="Major Positive Words", y="Frequency of Occurence",
title=paste0("Major Positive Words and Occurence for \n", finds[j], ", n = ", length(alltrans2[[j]]))) + 
theme(axis.text.x=element_text(angle=90))
p2 <- ggplot(dnfreq[[j]], aes(word, freq)) + geom_bar(stat="identity", fill="pink") + theme_bw() +
geom_text(aes(word, freq, label=freq), size=4) + labs(x="Major Negative Words", y="Frequency of Occurence",
title=paste0("Major Negative Words and Occurence for \n", finds[j], ", n = ", length(alltrans2[[j]]))) + 
theme(axis.text.x=element_text(angle=90))
cairo_pdf(paste0("posw", j, ".pdf"),  2+nrow(dpfreq[[j]])/8, 5)
plot(p1)
dev.off()
cairo_pdf(paste0("negw", j, ".pdf"),  2+nrow(dnfreq[[j]])/8, 5)
plot(p2)
dev.off()
}

Positive words for Putin
Negative words for Putin

Positive words for Baburin
Negative words for Baburin

Positive words for Grudinin
Negative words for Grudinin

Positive words for Zhirinovsky
Negative words for Zhirinovsky

Positive words for Sobchak
Negative words for Sobchak

Positive words for Surajkin
Negative words for Surajkin

Positive words for Titov
Negative words for Titov

Positive words for Yavlinsky
Negative words for Yavlinsky

Positive words for Yavlinsky
Negative words for Navalny

Fig. 3. Most frequent positive and negative words for candidates

We defined “most frequent” words as words whose count exceeded the total number of tweets divided by 200. There is just one small problem: such generic words as “like” and “right”, “well” etc. are on this table. We can remedy that by manually constructing a table of most popular meaningful positive and negative words for candidates (Table 1).

Candidate 1st + word 2nd + word 1st − word 2nd − word
Putin support 275 popular 252 problems 224 protests 153
Baburin important 30 right 27 curses 340 shit 235
Grudinin interesting 416 respect 266 dirty 232 poor 160
Zhirinovsky benefit 225 entertain 206 curses 339 whore 333
Sobchak useful 165 truthful 149 whore 571 beg 410
Surajkin free 86 regard 61 provocation 348 curses 339
Titov works 51 ready 30 shit 284 scandal 237
Yavlinsky liberty 91 great 83 sorry 339 shit 294
Navalny top 232 good 181 strike 355 boycott 352

Table 1. Two most frequent meaningful words for candidates

Conclusions

We can make several conclusions. First, several candidates (Baburin, Surajkin, Titov, Yavlinsky) are so unpopular that they were not mentioned 10 000 times over the course of 2.5 months since the beginning of 2018, which implies that they were added to the list of approved candidates to create the illusion of choice; they cannot compete against the obvious leader of the race. Next, on average, people tweet about politicians when they are discontent with something; it is reflected in the average negative score of tweets, except for Putin and Grudinin. Some politicians (Zhirinovsky, Sobchak) are viewed as politically promiscuous, which is reflected in the word frequency analysis. This all creates the illusion of choice presence, whereas most of the population of Russia does not have access to Twitter, or are not politically engaged, or are dependent on the subsidies issued by the present government, or cannot fully explore the majority of opposing opinions because they are carefully filtered out of the central and official media.

Bibliography

Bessi, Alessandro and Emilio Ferrara (2016). ‘Social bots distort the 2016 US Presidential election online discussion’.

Bhatia, Aankur (2015). Twitter Sentiment Analysis Tutorial. Rpubs.

Burgess, Jean and Axel Bruns (2012). ‘(Not) the Twitter election: the dynamics of the #ausvotes conversation in relation to the Australian media ecology’. Journalism Practice 6.3, pp. 384–402.

Kapko, Matt (2016). Twitter’s impact on 2016 presidential election is unmistakable. CIO.

Larsson, Anders Olof and Hallvard Moe (2012). ‘Studying political microblogging: Twitter users in the 2010 Swedish election campaign’. New Media & Society 14.5, pp. 729–747.

Sang, Erik Tjong Kim and Johan Bos (2012). ‘Predicting the 2011 dutch senate election results with twitter’. Proceedings of the workshop on semantic analysis in social media. Association for Computational Linguistics, pp. 53–60.

Wang, Hao et al. (2012). ‘A system for real-time twitter sentiment analysis of 2012 us presidential election cycle’. Proceedings of the ACL 2012 System Demonstrations. Association for Computational Linguistics, pp. 115–120.

Yang, Xiao, Craig Macdonald and Iadh Ounis (2017). ‘Using word embeddings in twitter election classification’. Information Retrieval Journal, pp. 1–25.

Гибель России

Сто лет назад погибла Россия. Не могу собраться с мыслями, поэтому привожу отрывки идей, приходящих в голову.

Читал «Солнце мёртвых» Шмелёва — слёзы наворачиваются на глаза. Как пел Тальков: «О, генеральская тетрадь, забытой правды возрожденье, как тяжело тебя читать обманутому поколенью». Нет больше той самой России, которая подарила миру блестящих писателей и композиторов, которые сформировали то, что все называют русской культурой. Горько от этого.

Отмечать столетие революции — значит радоваться гибели родной культуры.

Да, у меня крестьянские корни, моими предками были малороссийские крестьяне, поэтому, живи я в Российской империи, наиболее ярким событием всей жизни была бы смерть в 30 лет от тифа. Тем не менее, лучше бы уж я сто раз умер в Российской империи крестьянином, чем один раз родился в современной России. Если Бога нет, то кого мне благодарить за то, что хотя бы не родился в СССР?

Если бы у меня была возможность вызвать дух неизвестного белогвардейца и высказать ему все свои мысли, то я бы произнёс всего лишь два слова: «не уберегли».

А современный русский интеллигент мечтает сделать две вещи: устроить в России революцию и бежать во Францию.

О коммунистах

Во «Власти Народа» передовая: «Настал грозный час — гибнет Россия и Революция. Все на защиту революции, так ещё недавно лучезарно сиявшей миру!» — Когда она сияла, глаза ваши бесстыжие?

Ив. Бунин, «Окаянные дни».

Среди людей интеллигентных не принято высказываться о политике — заведено вежливо молчать. С другой стороны, во-первых, ваш покорный слуга никогда не претендовал на интеллигентность, а во-вторых, если все будут молчать (из врождённой незлобливости или от блестящего дворянского воспитания), то в таком случае, как сказал великий, «мы окрасим себя в те цвета, в которые мы окрасим себя» — будем иметь что имеем.

Сегодня получил весточку из России, однако, как оказалось, не самую приятную. КПРФ распространяет по почтовым ящикам листовки, в которых призывает голосовать за своих кандидатов в муниципальные депутаты в Москве.

Я не удивлён, что в политику лезет всякая сволота. Если кто-то задавался вопросом, почему российская экономика так слаба и неконкурентоспособна на международной арене, ответ лежит прямо у него в почтовом ящике. Кумачовые цареубийцы и сермяжные леворуционеры. Почему агитируют они, а стыдно мне? Как у них вообще хватает наглости в столетнюю годовщину великой русской катастрофы и гибели не только русского государства, но и многовековой русской культуры проповедовать коммунизм и ещё пытаться на нём нажиться?

St petersburg demonstration revolution

Однако по прочтении их предвыборной программы в мою душу закрались сомнения. Если в планах кандидатов не значится разжигать пожар мировой революции, то что это за коммунисты такие? Какое-то жалкое недоразумение, предатели собственной идеологии. Хороший коммунист — мёртвый коммунист!

О вреде дробления выборки

Предположим, изначально у нас было одно полное облако точек. Мы разделили его на три непересекающихся подмножества (обозначены цветами). В каждом из цветных облаков есть значимая взаимосвязь между переменными \(X\) и \(Y\), которую обнаруживают любые регрессионные методы (приведены три: классический МНК, квантильная регрессия при \(q=0{,}5\) и локально линейная регрессия первой степени).

Ложные значимые эффекты на подвыборках

Однако если взглянуть на изначальное облако из всех точек всех трёх цветов, то эффекта никакого не будет (эта ничтожно слабая и малозначимая зависимость показана толстой чёрной линией). Получается парадокс: если индивид относится к зелёной подвыборке, то в ней влияние \(X\) на \(Y\) есть. То же верно и для всех остальных подвыборок. Везде эффект положительный! Однако одновременно для всех точек никакого эффекта нет.

Зависимая переменная: Y
Вся выборка Синие Зелёные Красные
X 0.013 0.839*** 0.482*** 0.598***
(0.030) (0.024) (0.048) (0.046)
Константа 0.004 0.013 -1.202*** 1.245***
(0.031) (0.019) (0.055) (0.053)
Наблюдений 1000 386 307 307
R2 0.0002 0.756 0.252 0.361
Adjusted R2 -0.001 0.755 0.250 0.359
Residual Std. Error 0.981 (df = 998) 0.373 (df = 384) 0.667 (df = 305) 0.640 (df = 305)
F Statistic 0.198 (df = 1; 998) 1,187.609*** (df = 1; 384) 102.762*** (df = 1; 305) 172.266*** (df = 1; 305)
Note: *p<0.1; **p<0.05; ***p<0.01

Вывод: чем более избирательно исследователь будет составлять выборку для исследования («пожилое коренное население, которое до этого трудилось на работах, не требующих высокой квалификации»), тем больше вероятность, что он что-нибудь да обнаружит... и что эта находка будет ложной.

Код для репликации в R (отчего-то плагин глючит и склеивает всё в строку):

n <- 1000; set.seed(100); x <- rnorm(n); y <- rnorm(n); th <- 0.7; g1 <- (y < x + th) & (y > x - th);
g2 <- (y <= x - th); g3 <- (y >= x + th);
m <- lm(y~x); m1 <- lm(y~x, subset=g1); m2 <- lm(y~x, subset=g2); m3 <- lm(y~x, subset=g3)

Самое кошмарное музыкальное произведение

В Сети есть страница с очень интересным списком произведений, озаглавленная «The Darker Side of Classical Music» («Тёмная сторона классической музыки»). На ней собраны образцы наиболее трагичных произведений, которые вышибают у слушателей слезу или вселяют в них ужас. Некоторые записи в нём спорны (Шостакович?), некоторые не вызывают никакого сомнения (Шестая симфония Чайковского, «Плач по жертвам Хиросимы» Пендерецкого, «Остров мёртвых» Рахманинова). Как и в любом подобном списке, в нём есть фамилии забытых позднеромантических композиторов второго эшелона.

Однако одной записи в нём не хватает. Не хватает упоминания композитора, известного как «Чёрный Балакирев» благодаря разрушительной и мрачной силе, присутствующей в его произведениях. Это Сергей Михайлович Ляпунов, родной брат Александра Ляпунова. Он является автором двух симфоний и двух фортепианных концертов. Каждый из них играет тысячами разных красок: Первый концерт — классическая русская печаль, Второй концерт — новь, свежее дыхание, короткий, но изящный эксперимент с гармонией, Первая симфония — эпическое произведение, весь финал которого представляет собой пиррову победу, радость со слезами на глазах...

Однако у Ляпунова есть одно произведение, о котором почти никто не знает. Его почти никогда не исполняют. Во всём зарубежном интернете нет ни одной статьи, посвящённой ему. Это Вторая симфония — самая острая музыкальная катастрофа, чёрный кошмар, падение в пропасть безумия, олицетворение демона самоубийства и символ гибели России. Написанное в 1917 году, оно содержит максимальную концентрацию трагических чувств в первой части. Если у Скрябина первая часть Второй симфонии — это непрерывная борьба с Природой, напряжённая и предвещающая грозу, то у Ляпунова намного больше личного трагизма: мировая война, революция (после которой композитор эмигрировал в Париж), нежелание принять крах великой империи, приходящейся ему родной страной... Всегда бывший мрачным и угнетающим, во Второй симфонии композитор зашёл за грань разрушительного и разбивающего сердце.

Сегодня исполнился ровно год с того самого момента, как в библиотеке Cité Universitaire была совершена самая катастрофическая ошибка 2015 года — была открыта, прослушана и переслушана симфония Ляпунова №2 си-бимоль минор. Именно 18 апреля все демоны, роившиеся внутри этого сундука, вырвались на свободу. Автор убедительно просит неподготовленного читателя смириться с тем, что полчище ночных бесов будет подталкивает прослушивающего эту симфонию сброситься с моста, вспороть живот, взрезать горло или что-нибудь похуже. Все, кто не боится перейти на чёрную сторону искусства, приглашаются к прослушиванию этой музыкальной гибели целой страны только в высококачественных наушниках или перед полноценной акустической системой. Помощи вам ждать неоткуда, никто к вам не придёт. Если вдруг вы не сможете остановиться, заставьте кого-нибудь заставьте вас поклясться самому/самой себе, что никогда в жизни вы больше не переслушаете этого произведения.

Экспонента Ляпунова / Lyapunov Exponent / Exposant de Liapounov

У композитора Ляпунова произведения настолько мрачные и сокрушающие, что экспонента Ляпунова отрицательная, но хаос при этом растёт!

The music of composer Lyapunov is so dark and crushing that Lyapunov’s exponent is negative, but his chaos is growing!

La musique de compositeur Liapounov est si sombre et écrasante que son exposant est négatif mais son chaos s’accroît!

P.S. I would not mind if someone translated this into German so that we could witness four spellings (together with Ljapunow) in one post.

«Школьная» задача про возраст членов семьи

Три версии одной и той же задачки. Числа и формулировки немного разные, ответ одинаковый.

  • Вася младше своей мамы на 21 год. 15 лет назад отношение возраста Васи к разнице их с мамой возрастов было меньше возраста мамы в то время ровно в 9 раз. Вопрос: где сейчас Васина бабушка?
  • Вася младше своей мамы на 28 лет. 8 лет назад отношение возраста Васи к разнице их с мамой возрастов составляло ровно 15 % возраста мамы в то время. Вопрос: где сейчас Васин дедушка?
  • Вася младше своей мамы на 24 года. 18 лет назад отношение возраста Васи к разнице их с мамой возрастов составляло ровно 9,5 % возраста мамы в то время. Вопрос: где сейчас Васина бабушка?


Матожидание ошибок при симуляции МНК

Я больше года мучился, не понимал, почему аддитивность/мультипликативность нормальных ошибок играет такую большую роль, и даже разъяснение преподавателя не помогало. Почти год назад я написал этот пост: http://kostyrka.ru/blog/archives/1246 — и, оказывается, впустую! Сегодня на эконометрике ко мне пришло видение этой проблемы в свете того, что матожидание от функции не равно функции от матожидания. Конкретно это имеет значение, если вас просят оценить уравнение, в котором ошибка мультипликативна и, следовательно, при логарифмировании её матожидание должно дать ноль. Что делает плохой студент? Генерирует мультипликативную ошибку со средним 1 из первого попавшегося распределения!

Предположим, вы хотите научиться оценивать уравнение
\[Q = a K^\alpha L^\beta \cdot \varepsilon,\]
где \(\mathbb{E}(\varepsilon\mid K, L)=1\). Уже и компьютер включили, и gretl запустили. Какое распределение \(\varepsilon\) надо взять?

По прочтении условия очень хочется взять распределение ошибок \(\chi^2_1\), так как \(\mathbb{E}(\chi^2_1)=1\). Ещё очень хочется взять \(\mathcal{N}(1;\sigma^2)\). Некоторые кулибины берут \(\mathcal{U}[0{,}5;1{,}5]\) или \(\mathcal{U}[0;2]\). Так почему же нельзя брать ни то, ни другое, ни третье?

Ответ: так как по МНК уравнение будет оцениваться в логарифмах, то важно, чтобы не \(\mathbb{E}(\varepsilon)\) было единицей, а \(\mathbb{E}(\ln \varepsilon)\) было нулём. Матожидание не всепроникающее, поэтому матожидание логарифма не равно логарифму матожидания. Если бы выполнялась такая глупость, что \(\mathbb{E}\bigl(f(X)\bigr) = f \bigl( \mathbb{E}(X) \bigr)\), то было бы и \(\mathbb{E}(\ln X) = \ln \mathbb{E}(X)\), и \(\mathbb{E}(X^2) = \bigl(\mathbb{E}(X)\bigr)^2\), дисперсия стала бы тождественным нулём, и наступил бы конец света.

 

Хи-квадрат после логарифмирования начнёт себя вести очень плохо. Настолько плохо, что формулу для его матожидания я запишу в попустительской нотации:
\[ \varepsilon \sim \chi^2_1 \quad \Rightarrow \quad \mathbb{E}(\ln \varepsilon) = \mathbb{E}\bigl(\ln \mathcal{N}^2(0;1)\bigr) =\mathbb{E}\bigl(2\ln |\mathcal{N}(0;1)|\bigr) = 2\mathbb{E}\bigl(\ln |\mathcal{N}(0;1)|\bigr) \]

Во-первых, отрицательные значения перейдут вправо (распределение аргумента станет положительным). Во-вторых, из оставшихся логарифмов ошибок более 2/3 будут отрицательными (вероятность, что величина из нормального распределения по модулю будет меньше единицы, равна 68,27 %), причём некоторые из них будут убийственно отрицательными: логарифм близкой к нулю величины уходит глубоко под землю. Если уж идти до конца, то
\[ \varepsilon \sim \chi^2_1 \quad \Rightarrow \quad \mathbb{E}(\ln \varepsilon) = 2\mathbb{E}\bigl(\ln |\mathcal{N}(0;1)|\bigr) = \sqrt{\frac{2}{\pi}} \int\limits_{-\infty}^{+\infty} \ln x \cdot e^{ -\frac{x^2}{2}} \,\mathrm{d}x = -\gamma -\ln 2 \approx -1{,}270\,36, \]
где \(\gamma\) — постоянная Эйлера—Маскерони.

В общем случае матожидание логарифма хи-квадрата с \(k\) степенями свободы выражается через полигамма-функцию (даже дигамма-функцию):
\[ \mathbb{E}(\chi^2_k) = \ln 2 + \psi^{(0)}\left(\frac{k}{2} \right), \quad \psi^{(0)}(x) \equiv \frac{\Gamma'(x)}{\Gamma(x)} \]
Biased log of chi square
Если хотите, чтобы матожидание логарифма было равно нулю, то надо брать хи-квадрат с \(1{,}866\,025\) степенями свободы.

 

Нормальное распределение может выдать абсолютно любые значения случайной величины, так как функция плотности определена на \(\mathbb{R}\). Поэтому если ошибка мультипликативна и нормальна, то, вообще говоря, пропадают все значения, где ошибка получилась меньше нуля. Кроме того, если ошибка распределена как \(\mathcal{N}(1;\sigma^2)\), а наивные пользователи думают, что её логарифм в среднем даст ноль, то спешу их разочаровать следующей картиной:
Biased Log of Normal Distribution
Здесь avgleps — это \(\overline{\ln \varepsilon_i} = \mathbb{E}(\ln\varepsilon_i \mid \sigma)\), где \(\varepsilon_i \sim \mathcal{N}(1;\sigma^2)\). Любой желающий может в этом убедиться (если скрипт виснет, надо уменьшить размер выборки до, скажем, nulldata 5000):
nulldata 100000
scalar step=0.01
#
loop for (se=0.001; se<10; se+=step) --progressive series eps = randgen(N,1,se) series leps = ln(eps) genr avgleps = mean(leps) store biasedln.gdt se avgleps endloop # open biasedln.gdt gnuplot avgleps se --with-lines --output=display --suppress-fitted

На самом деле безусловное матожидание логарифма нормального распределения — это следующее аналитическое выражение:
\[ \varepsilon \sim \mathcal{N}(1;\sigma^2) \quad \Rightarrow \quad \mathbb{E}\bigl(\ln \varepsilon \bigr) = \frac{1}{2} \left(-{}_1\mathrm{F}_1\left(0;\frac{1}{2};-\frac{1}{2 \sigma ^2}\right)+\ln \left(\frac{\sigma ^2}{2}\right)-\gamma \right) + \frac12 i \pi \mathrm{erfc}\left(\frac{1}{\sqrt{2} \sigma}\right), \]
где \({}_1\mathrm{F}_1\) — вырожденная гипергеометрическая функция Куммера (Kummer confluent hypergeometric function).
Biased expectation
Чтобы избавиться от мнимой единицы, некоторые товарищи могут взять условное матожидание (\(x>0\)) или выкинуть все отрицательные значения (как на графике в gretl’е), то и тогда у них матожидание будет равно ещё более жуткому выражению (раскрываю свои карты) — и притом обе эти функции будут давать абсолютно одинаковые графики:
\[ \text{Expectation}[\log (x)\unicode{f3d3}x>0,x\approx \text{NormalDistribution}[1,\sigma ]] \]
\[ \frac{e^{-\frac{1}{2 \sigma ^2}} \left(-e^{\frac{1}{2 \sigma ^2}} \sigma \text{Hypergeometric1F1}^{(1,0,0)}\left(0,\frac{1}{2},-\frac{1}{2 \sigma ^2}\right)+\sqrt{\frac{2}{\pi }} \text{Hypergeometric1F1}^{(1,0,0)}\left(1,\frac{3}{2},\frac{1}{2 \sigma ^2}\right)+\gamma e^{\frac{1}{2 \sigma ^2}} \sigma \text{erfc}\left(\frac{1}{\sqrt{2} \sigma }\right)-e^{\frac{1}{2 \sigma ^2}} \sigma \log (2) \text{erfc}\left(\frac{1}{\sqrt{2} \sigma }\right)-2 e^{\frac{1}{2 \sigma ^2}} \sigma \text{erfc}\left(\frac{1}{\sqrt{2} \sigma }\right) \log (\sigma )-2 \gamma e^{\frac{1}{2 \sigma ^2}} \sigma +4 e^{\frac{1}{2 \sigma ^2}} \sigma \log (\sigma )\right)}{2 \sigma \left(\text{erf}\left(\frac{1}{\sqrt{2} \sigma }\right)+1\right)} \]

Вам это надо? Не надо. Симуляция уже показала вам, насколько велико смещение. Если \(\sigma\not\approx 1{,}092\,340\) (приблизительное решение этой страшной аналитической вещи относительно \(\sigma\)), то наличествует систематическая ошибка, и коэффициенты модели неверны, причём по вышеприведённым графикам можно оценить смещение. Даже если смещение равно 0,1, но наблюдений в выборке 100 000, то...

Совсем плохой случай: если в исходной модели ошибки мультипликативны и нормальны с нулевым матожиданием и дисперсией \(\sigma^2\), то при переходе к логарифмам их матожидание станет равным \(\frac{1}{2} \left(-\gamma + \ln \frac{\sigma^2}{2} \right)\), смещение принимает какое угодно значение, смещается оценка свободного члена, и ни при какой огромной выборке остальные истинные коэффициенты получить не удастся!

 

Если кто-то возьмёт равномерное распределение ошибок с матожиданием 1, то ясно, что для получения осмысленных ошибок нижняя граница интервала распределения может изменяться от 0 до 1, а верхняя — от 2 до 1 (зеркально). Посмотрим на матожидание ошибки при различных параметрах генерирования:
Biased Log of Uniform Distribution
nulldata 100000
scalar step=0.001
#
loop for (bnd=0.001; bnd<1; bnd+=step) --progressive series eps = randgen(u,bnd,2-bnd) series leps = ln(eps) genr avgleps = mean(leps) store biasedln.gdt bnd avgleps endloop # open biasedln.gdt gnuplot avgleps bnd --with-lines --output=display --suppress-fitted

Уже лучше, но всё равно плохо. Если ошибка имеет равномерное распределение от \(b\) до \((2-b)\), то её среднее равно одному, однако среднее её логарифма нулю никак не равно. А вот чему оно равно:
\[\varepsilon \sim \mathcal{U}[b;2-b] \quad \Rightarrow \quad \mathbb{E}\bigl(\ln \varepsilon\bigr) = \frac{-2 b+b \ln (2-b)+b \ln b-2 \ln (2-b)+2}{2 (b-1)} \]
Интересная функция. Её предел в нуле равен \(\ln 2 - 1 \approx -0{,}307\), а при стремлении к единице её значение стремится к нулю. Однако это значит, что для минимизации смещения требуется брать значения границ, равные \([0{,}95;1{,}05]\) или ещё у́же, а до такого додумается далеко не каждый: все будут бояться, что при таком малом разбросе ошибок получатся гигантские (\(\geqslant50\)) t-статистики, а сама регрессия потеряет всякий смысл, исчезнет случайность, а «наблюдаемые» значения будут лежать почти в одной гиперплоскости. При этом все забывают, что только лишь очень близкие к единице \(\varepsilon\) позволяют задействовать отношение эквивалентности \(\ln (1 + \delta) \sim \delta\) при \(\delta\approx 0\).
Biased log of uniform distribution

 

Мораль 1. При генерировании искусственных данных помните, что в модели с мультипликативными ошибками очень желательна нормальность их логарифмов с центром массы в нуле. Если матожидание ошибок, преобразованных в аддитивные, не равно нулю, коэффициенты МНК будут в порядке, но оценка константы будет смещённой и несостоятельной, что критично при оценке технологического фактора в модели Кобба—Дугласа.

Мораль 2. Решаете задачку, гоняете циферки, а вам нужны ошибки, которые не сместят коэффициентов? Берите \(\varepsilon = \exp\bigl(\mathcal{N}(0;1)\bigr)\).

 

P.S. Мне кажется, что при симуляции Монте-Карло имеет смысл поиграться с функциональной формой ошибок и добиться того, чтобы проверка выполнения условия \(\sum(y_i^* - {\boldsymbol{x}^*_i}'\boldsymbol\beta)=0\) начиналась на шаг ранее, когда исходная форма \(y_i = f(\boldsymbol{x}_i, \boldsymbol\beta, \varepsilon_i)\) преобразуется в \(y_i^* = f^{-1}(y_i) = f^{-1}\bigl(f(\boldsymbol{x}_i, \boldsymbol\beta, \varepsilon_i)\bigr) = {\boldsymbol{x}^*_i}'\boldsymbol\beta + \varepsilon_i^*\), и чтобы особое внимание уделялось условию \(\mathbb{E}(\varepsilon_i^*)=0\). Конечно, это не так важно, но всё-таки есть такое эмпирическое правило, что наличие значимой константы говорит о пропущенных переменных; поэтому если в истинной модели есть константа, но систематическая ошибка её сильно снижает, то может возникнуть ложное ощущение того, что в модели учтено достаточное количество влияющих факторов, в то время как друг на друга накладываются большая необъяснённая остаточная вариация и систематическая ошибка.

seed не должен быть ручным

Правило 1. Для того чтобы любой эксперимент Монте-Карло был воспроизводим, генератору псевдослучайных чисел необходимо сообщить seed — начальное число, с которого пойдёт поток чисел. Если при использовании одного и того же алгоритма я перед каждой процедурой генерирования случайных чисел задаю тот же seed, что и вы, то у нас не могут получиться разные результаты. В этом и состоит псевдослучайность.

Правило 2. Если вам нужно получить несколько независимых случайных величин, не устанавливайте один и тот же seed. Для воспроизводимости принципиально установить его вообще. Если в исследовании требуются три случайные переменные: \(X_1\), \(X_2\), \(X_3\) — и чтобы каждая имела распределение \(\mathcal{U}[\underline{x}\vphantom{x}_i;\overline{x}\vphantom{x}_i]\), ни в коем случае не пишите так:

set seed 100
series K=randgen(u,10,30)
set seed 100
series L=randgen(u,20,40)
set seed 100
series M=randgen(u,-50,50)

Это приведёт к вырождению процесса и к тому, что между случайными величинами будет абсолютная мультиколлинеарность (они будут равны друг другу с точностью до линейного преобразования), а коэффициент корреляции будет равен 1. Это ещё хуже, чем компьютер RANDU, у которого «каждое число случайно само по себе, но не гарантируется того же для большего их количества», а единичная корреляция следует из соотношения \(x_{k+2}=6x_{k+1}-9x_{k}\).

degen-random

Хороший эконометрист всегда задаёт большие и как можно более отличные друг от друга сиды и записывает их в надёжное место. Отличный эконометрист генерирует случайные сиды и записывает начальный сид для генератора сидов; отличный эконометрист всегда должен мыслить на один уровень случайности выше.

set seed 100
series K1=randgen(u,10,30)
set seed 200
series L1=randgen(u,20,40)
set seed 300
series M1=randgen(u,-50,50)

A truly random 3-dimensional uniform distribution

Не надо думать, что зиккурат-алгоритм или алгоритм Бокса—Мюллера вывезет. Не вывезет!

Если вам необходимо прогнать цикл из 10 000 симуляций, установите сид для первой симуляции и не трогайте его, пока не закончится десятитысячная. У современных генераторов достаточно длинные периоды, чтобы не зациклиться даже на миллионе циклов. Алгоритм KISS («Keep it Simple Stupid», или «Не усложняй, придурок») обладает периодом, бо́льшим на 40 порядков, чем число атомов во Вселенной. Если вам необходимо воспроизвести 6 234-ю итерацию, не надо задавать вручную все 10 000, а затем выбирать 6 234-й, нет! Достаточно после каждого цикла в некоторую переменную записывать состояние датчика случайных чисел (random number generator state, или начальное значение, которое он выбрал себе сам для новой итерации), а затем для воспроизведения 6234-й итерации взять seed из полученной переменной. Не обесчещивайте славное имя Джорджа Марсальи, посмотрите в руководство пользователя или исходный код, найдите переменную, в которую записывается состояние датчика, и копируйте её значения в летописи.

Вторая причина, по которой не надо устанавливать seed вручную, — это плохая «случайность» чисел, получаемых с ручным сидом. Если вы получили 50 000 значений случайной переменной и хотите получить ещё 50 000, надо быть волшебником, чтобы при выставлении второго сида не наткнуться на начальное значение, которое уже проскакивало в первом цикле итераций, иначе части первой и второй последовательностей псевдослучайных чисел могут совпасть! Это смертный приговор специалисту по временным рядам или исследователю систем бинарных уравнений, где у ошибок-инноваций не должно быть корреляции со всеми предыдущими значениями случайной переменной. А у вас стоит идеальная машина, изготовленная по технологическому процессу 22 нм (диаметр среднего атома всего лишь в сто раз меньше), куда подаётся ток совершенно определённой силы, поэтому положение каждого электрона в машине детерминировано, и это не даёт возможности получить истинно случайные числа. Вот что вы будете делать с абсолютной корреляцией и единичными корнями, которые возникли только потому, что вы своим указующим перстом установили seed, совпавший с тем, который использовался для 49 998-го значения? Л’Экюйера и Симара на вас не хватает (TestU01, Pierre L’Ecuyer, Richard Simard).

Гениальный сарказм

Как-то раз возле МГУ мне попалась в руки вот такая листовка. Написана она якобы от лица гуманитария. Каждое предложение вызывало у меня гомерический хохот, поэтому я не мог не поделиться этим шедевром с окружающим миром. Прикладываю оригинальный снимок, орфография и пунктуация сохранены.
sarcastic-note

Не понравилось? Перечитайте ещё раз. Перед вами блестящее подражание стилю письма выпускников вузов средней руки. Автор брошюры неизвестен, но так остроумно высмеять недовыпускников мог лишь человек недюжинного остроумия; возможно, профессиональный писатель-сатирик, приближающийся к уровню Жванецкого. Давайте разберём этот текст.

  • Орфография и пунктуация. В точности воспроизведены наиболее распространённые ошибки: пропущенные запятые, призирают (никто на них не собирается смотреть сочувственно и милосердно), НЕ обязательно (дешёвый крик заглавных букв и лишний пробел), 30-и (лень набрать полное числительное или посмотреть, как писать окончания в смешанной форме) и т. д. Самая вкусная еда — двойные пробелы, некорректные тире и кавычки, зверские абзацные отступы, оторванные предлоги и числа, пляшущий размер шрифта, преступное ручное форматирование.
  • Интеллектуальное слабоумие, интеллектуальная подлость. Да, это чувствуется. Я даже теряюсь в догадках, что такое «интеллектуальная подлость».
  • Разъяснения: грамотно (без ошибок), НЕ обязательно иметь мозги, ум. Без пояснений просто никуда. Даже и не догадались бы, что грамотно — это без ошибок, а ум связан с мозгами.
  • Чудовищное разбиение предложений, оторванные придаточные; деепричастие так и просит лица.
  • ...засоряют информационное поле человечества, гробят лес. Труды гробят лес, да. Об этом надо обязательно было сказать в конце!
  • Идеи сотовой связи. Без комментариев.
  • Ничего востребованного НЕ рожаем. Может, рождаем? Или это от слова «рожа»?
  • Слишком много чести «гуманитариям» полагать, что они создали коммунизм и фашизм.
  • Диагноз: «Открытой или скрытой формы интеллектуальной дебильности, переходящей в интеллектуальную подлость». Ещё раз интеллектуальная подлость. «Проповедник обязан иметь сердце сокрушенно...»
  • Пожизненное ля-ля-ля.
  • Отползайте от нас слабоумных гуманитариев, учите логику. Интересные призывы в одном предложении.
  • Игнорируйте публикации лиц с низким “IQ”. Ага. Как будто коэффициент IQ является хорошим индикатором отсутствия «интеллектуальной подлости».
  • Рейтинг приближается к плинтусу. Никогда ещё рейтинг не сравнивали с плинтусом. Не уровень, а именно сам рейтинг приближается конкретно к плинтусу.
  • ...поклоняющихся не законам природы. Оказывается, интеллектуалы должны поклоняться законам природы? Это как?
  • ...«картинкам» от пьяных художн. Коронное завершение номера. Надеюсь, ни один художн. не обиделся по прочтении сего фельетона.

Как сказал бы Жванецкий, при взгляде на этот текстик хочется вызывать уже не группу психологов, успокаивающих под девизом «то ли ещё будет», а настоящий мотовзвод огнестрельного сочувствия.